feat: 쿠키 통한 로그인 권한 체크 구현
This commit is contained in:
+12
-47
@@ -1,49 +1,14 @@
|
||||
"use client";
|
||||
import "./globals.css";
|
||||
import i18next from "i18next";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import koBase from "../public/locales/ko/message.json";
|
||||
import { StoreProvider } from "./store";
|
||||
import { LoginApi } from "@/src/entities/auth";
|
||||
import { useEffect } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { App } from "../src/app";
|
||||
|
||||
const i18n = i18next.createInstance();
|
||||
i18n.init({
|
||||
lng: "ko",
|
||||
fallbackLng: "ko",
|
||||
resources: {
|
||||
ko: {
|
||||
base: koBase,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 하수관로",
|
||||
description: "디지털 하수관로 시스템입니다",
|
||||
keywords: ["하수관로"],
|
||||
openGraph: {
|
||||
title: "디지털 하수관로",
|
||||
description: "디지털 하수관로 시스템입니다"
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
useEffect(() => {
|
||||
// 렌더 중이 아니라 클라이언트에서만 실행 + 에러 캐치
|
||||
void LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" })
|
||||
.finally(()=>{
|
||||
// getCommonUpCd()
|
||||
} )
|
||||
.catch((e) => {
|
||||
console.error("Auto login failed:", e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<StoreProvider>
|
||||
{children}
|
||||
</StoreProvider>
|
||||
</I18nextProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
export default App;
|
||||
+24
-159
@@ -1,166 +1,31 @@
|
||||
"use client";
|
||||
import { Button } from "@common/components/ui";
|
||||
|
||||
import { LoginApi } from "../src/entities/auth";
|
||||
import { useEffect } from "react";
|
||||
import { getCookie } from "../src/shared/lib";
|
||||
import { AUTH_USER_ID } from "../src/shared/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-8 p-6">
|
||||
{/* 시설 등록 */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">시설 등록</h2>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/fcltReg/pbnstReg')}}>
|
||||
하수재이용 시설 등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/fcltReg/rainReg')}}>
|
||||
빗물재이용 시설 등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/fcltReg/rswtReg')}}>
|
||||
중수도시설 등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/fcltReg/htdflReg')}}>
|
||||
온배수시설 등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
const userId = "CLEANWATER";
|
||||
const password = "gktneh12!@";
|
||||
const checkToken = getCookie(AUTH_USER_ID);
|
||||
|
||||
{/* 지도점검 결과 */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">지도점검 결과</h2>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/prfmncReg/pbnstPrfmncReg')}}>
|
||||
하수재이용실적 등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn')}}>
|
||||
하수재이용실적 현황
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/prfmncReg/chkgdnReg')}}>
|
||||
지도점검 결과 등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/prfmncSttn/chkgdnSttn')}}>
|
||||
지도점검 결과 현황
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 시설 현황 */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">시설 현황</h2>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/fcltSttn/pbnstSttn')}}>
|
||||
하수재이용시설현황
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/fcltSttn/rainSttn')}}>
|
||||
빗물재이용시설현황
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/fcltSttn/rswtSttn')}}>
|
||||
중수도이용시설현황
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/ruseFcltMng/fcltSttn/htdflSttn')}}>
|
||||
온배수현황
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수요처 관리 */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">수요처 관리</h2>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/matMng/coMng/coSttn')}}>
|
||||
수요처 관리
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/matMng/coMng/ldrmntCoSttn')}}>
|
||||
잠재수요처 관리
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/matMng/coMng/drgtSttn')}}>
|
||||
우리동네가뭄
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수요처 매칭 */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">수요처 매칭</h2>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/matMng/coMat/coMatRvw')}}>
|
||||
수요처 매칭 검토
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/matMng/coMat/ruseRvwHstry')}}>
|
||||
재이용검토이력
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/matMng/coMat/ruseCaseMng')}}>
|
||||
물재이용사례 관리
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 통계*/}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">통계 매칭</h2>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/stats/rainStat')}}>
|
||||
빗물재이용시설통계
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/stats/rswtStat')}}>
|
||||
중수도시설통계
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 시설 현황(지도) */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">시설 현황(지도)</h2>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Button className="w-full" onClick={() => {router.push('/map/mainMap')}}>
|
||||
시설 현황(지도)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!checkToken) {
|
||||
async function doLogin() {
|
||||
try {
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/map/mainMap";
|
||||
} catch (err) {
|
||||
console.error("로그인 실패", err);
|
||||
}
|
||||
}
|
||||
doLogin();
|
||||
} else {
|
||||
router.push("/map/mainMap");
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -29,10 +29,6 @@ export function middleware(req: NextRequest) {
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
if (pathname === "/") {
|
||||
return NextResponse.redirect(new URL("/map/mainMap", req.url));
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/cms-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const Authorization = req.cookies.get(CLAIM_NAME)?.value;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
return (
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<body>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export { App };
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { DndProvider } from "react-dnd";
|
||||
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 { AuthWrapper } from "../../entities/auth";
|
||||
import { MenuWrapper } from "../../entities/menu";
|
||||
import { PromptProvider, StoreProvider } from "../../shared/providers";
|
||||
import { AlertDialog } from "../../shared/ui/alert-dialog";
|
||||
import { PromptDialog } from "../../shared/ui/prompt-dialog";
|
||||
import { ConfirmDialog } from "../../shared/ui/confirm-dialog";
|
||||
// import "./globals.css";
|
||||
import i18next from "i18next";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import koBase from "@/public/locales/ko/message.json";
|
||||
import { StoreProvider as WrStoreProvider } from "@/app/store";
|
||||
import { LoginApi } from "@/src/entities/auth";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
||||
const i18n = i18next.createInstance();
|
||||
i18n.init({
|
||||
lng: "ko",
|
||||
fallbackLng: "ko",
|
||||
resources: {
|
||||
ko: {
|
||||
base: koBase,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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()}
|
||||
>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<WrStoreProvider>
|
||||
{children}
|
||||
</WrStoreProvider>
|
||||
</I18nextProvider>
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
<div className="text-2xl">
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { AppProvider };
|
||||
@@ -0,0 +1 @@
|
||||
export { AppProvider } from "./app-provider";
|
||||
@@ -0,0 +1,12 @@
|
||||
@import "@common/assets/styles/base.css";
|
||||
@import "@common/assets/styles/diveicon.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: "Noto Sans KR", sans-serif;
|
||||
font-size: 10px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export { LoginApi } from "./api/login-service";
|
||||
//export { AuthWrapper } from "./lib/auth-wrapper";
|
||||
export { AuthWrapper } from "./lib/auth-wrapper";
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { client } from "../../../shared/api";
|
||||
import type { IUserInfo } from "../../../shared/store";
|
||||
import { useUserStore } from "../../../shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "../../../shared/config";
|
||||
import { getCookie } from "../../../shared/lib";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
|
||||
const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL || "http://localhost:3000/auth/login"; // 로그인 페이지 URL
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const response = await client.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
|
||||
);
|
||||
if (!userInfo) setUserInfo(response.data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
router.push("/"); // 개발시 홈으로 돌아가기
|
||||
|
||||
// window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트
|
||||
// router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { AuthWrapper };
|
||||
@@ -0,0 +1 @@
|
||||
export { MenuAuthorityApi } from "../api/menu-authority-service";
|
||||
@@ -0,0 +1 @@
|
||||
export { AuthorityButton } from "../ui/authority-button";
|
||||
@@ -0,0 +1,40 @@
|
||||
import { client } from "@/src/shared/api";
|
||||
import { ENDPOINT } from "@/src/shared/config";
|
||||
import type {
|
||||
MenuAuthorityList,
|
||||
MenuAuthorityParams,
|
||||
MenuAuthorityPayload,
|
||||
MenuAuthorityService,
|
||||
PageMenuAuthority,
|
||||
} from "../model/menu-authority-interface";
|
||||
|
||||
export class MenuAuthorityApiService implements MenuAuthorityService {
|
||||
public async getMenuAuthorityList(
|
||||
params: MenuAuthorityParams,
|
||||
): Promise<MenuAuthorityList> {
|
||||
const response = await client.get<MenuAuthorityList>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPageMenuAuthority(
|
||||
menuCd: number,
|
||||
): Promise<PageMenuAuthority[]> {
|
||||
const response = await client.get<PageMenuAuthority[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuAuthorityApi = new MenuAuthorityApiService();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { AuthorityButton } from "./ui/authority-button";
|
||||
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface MenuAuthorityParams extends PageableParams {
|
||||
groupId?: string;
|
||||
menuCd?: number;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface PageMenuAuthority {
|
||||
mngAuthrtYn: string;
|
||||
inqAuthrtYn: string;
|
||||
regAuthrtYn: string;
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
}
|
||||
|
||||
export const menuAuthoritySchema = z.object({
|
||||
groupId: z.string(),
|
||||
groupNm: z.string(),
|
||||
menuCd: z.number(),
|
||||
mngAuthrtYn: z.string(),
|
||||
inqAuthrtYn: z.string(),
|
||||
regAuthrtYn: z.string(),
|
||||
mdfcnAuthrtYn: z.string(),
|
||||
delAuthrtYn: z.string(),
|
||||
useYn: z.string(),
|
||||
mode: z.string(),
|
||||
});
|
||||
|
||||
export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({
|
||||
groupNm: true,
|
||||
});
|
||||
|
||||
export const menuAuthorityFormSchema = z.object({
|
||||
menuAuthority: z.array(menuAuthorityPayloadSchema),
|
||||
});
|
||||
|
||||
export type MenuAuthority = z.infer<typeof menuAuthoritySchema>;
|
||||
export type MenuAuthorityList = PageableList<MenuAuthority>;
|
||||
export type MenuAuthorityPayload = z.infer<typeof menuAuthorityPayloadSchema>;
|
||||
export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
|
||||
|
||||
export interface MenuAuthorityService {
|
||||
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
|
||||
getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>;
|
||||
saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type {
|
||||
MenuAuthority,
|
||||
MenuAuthorityFormData,
|
||||
MenuAuthorityPayload,
|
||||
} from "./menu-authority-interface";
|
||||
|
||||
export const createColumns = (
|
||||
form: UseFormReturn<MenuAuthorityFormData>,
|
||||
): ColumnDef<MenuAuthority>[] => {
|
||||
const getCurrentRow = (i: number) => form.watch("menuAuthority")[i];
|
||||
|
||||
const handleCheckedChange =
|
||||
(i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => {
|
||||
if (getCurrentRow(i)?.mode === "R") {
|
||||
form.setValue(`menuAuthority.${i}.mode`, "U");
|
||||
}
|
||||
form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N");
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: "groupNm",
|
||||
header: "그룹명",
|
||||
},
|
||||
{
|
||||
accessorKey: "mngAuthrtYn",
|
||||
header: "관리권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mngAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"regAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"mdfcnAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"delAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "regAuthrtYn",
|
||||
header: "등록권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.regAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnAuthrtYn",
|
||||
header: "수정권한 ",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mdfcnAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "mdfcnAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "delAuthrtYn",
|
||||
header: "삭제권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.delAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "delAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "inqAuthrtYn",
|
||||
header: "조회권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.inqAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "inqAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { Button, cn, type ButtonProps } from "@common/components";
|
||||
import type { PageMenuAuthority } from "../model/menu-authority-interface";
|
||||
|
||||
type AuthorityButtonProps = {
|
||||
authority: keyof PageMenuAuthority;
|
||||
} & ButtonProps;
|
||||
|
||||
function AuthorityButton({
|
||||
authority,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AuthorityButtonProps) {
|
||||
const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority);
|
||||
|
||||
const isAuthority = pageMenuAuthority?.[authority] === "Y";
|
||||
|
||||
return (
|
||||
<Button className={cn(className, !isAuthority && "hidden")} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { AuthorityButton };
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import {
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthorityFormData,
|
||||
type MenuAuthorityPayload,
|
||||
} from "../model/menu-authority-interface";
|
||||
import { createColumns } from "../model/menu-authority-table-columns";
|
||||
import { MenuAuthorityApi } from "../api/menu-authority-service";
|
||||
|
||||
type ManagementAuthorityDialogProps = {
|
||||
menuCd?: number;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function ManagementAuthorityDialog({
|
||||
menuCd,
|
||||
...props
|
||||
}: ManagementAuthorityDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: menuAuthorityList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd],
|
||||
queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }),
|
||||
enabled: !!menuCd,
|
||||
});
|
||||
|
||||
const { mutateAsync: saveMenuAuthority } = useMutationWrapper({
|
||||
mutationFn: (data: MenuAuthorityPayload[]) =>
|
||||
MenuAuthorityApi.saveMenuAuthority(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuAuthorityFormData>({
|
||||
resolver: zodResolver(menuAuthorityFormSchema),
|
||||
values: {
|
||||
menuAuthority: menuAuthorityList?.content ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: menuAuthorityList?.content ?? [],
|
||||
columns: createColumns(form),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const data = form.getValues("menuAuthority");
|
||||
const filteredData = data.filter((item) => item.mode !== "R");
|
||||
await saveMenuAuthority(filteredData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3" disabled={!menuCd}>
|
||||
<i className="diveicon di-personal-information" />
|
||||
권한관리
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[40rem] overflow-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ManagementAuthorityDialog };
|
||||
@@ -0,0 +1,14 @@
|
||||
import { client } from "@/src/shared/api";
|
||||
import { ENDPOINT } from "@/src/shared/config";
|
||||
import type { MenuItem, MenuService } from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
async getMenuRoles(siteId: string): Promise<MenuItem[]> {
|
||||
const response = await client.get<MenuItem[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
export type { MenuItem } from "./model/menu-interface";
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { MenuAuthorityApi } from "@/src/entities/menu-authority/@x/menu";
|
||||
import { useAlertStore, useMenuStore } from "@/src/shared/lib";
|
||||
import { getCookie } from "@/src/shared/lib";;
|
||||
import { SITE_ID } from "@/src/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import { getCurrentMenu } from "./utils";
|
||||
|
||||
const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } =
|
||||
useMenuStore((state) => state);
|
||||
|
||||
const getCurrentMenuInfo = async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
if (!menuRoles) return;
|
||||
return getCurrentMenu(menuRoles, pathname ?? "");
|
||||
};
|
||||
|
||||
const getPageMenuAuthority = async () => {
|
||||
const menuCd = getCookie("menu-code-key");
|
||||
if (!menuCd) return;
|
||||
const response = await MenuAuthorityApi.getPageMenuAuthority(
|
||||
parseInt(menuCd),
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
const checkInquiryAuthority = () => {
|
||||
return pageMenuAuthority?.inqAuthrtYn === "Y";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const currentMenu = await getCurrentMenuInfo();
|
||||
if (currentMenu) setCurrentMenuInfo(currentMenu);
|
||||
|
||||
const response = await getPageMenuAuthority();
|
||||
if (response?.length) setPageMenuAuthority(response[0]!);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageMenuAuthority) return;
|
||||
const inquiryAuthority = checkInquiryAuthority();
|
||||
if (!inquiryAuthority)
|
||||
alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/"));
|
||||
}, [pageMenuAuthority]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { MenuWrapper };
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { MenuItem } from "../model/menu-interface";
|
||||
|
||||
export const getCurrentMenu = (
|
||||
menus: MenuItem[],
|
||||
pathname: string,
|
||||
): MenuItem | null => {
|
||||
for (const menu of menus) {
|
||||
if (
|
||||
menu.urlPath !== "" &&
|
||||
(menu.urlPath === pathname || pathname.startsWith(menu.urlPath))
|
||||
)
|
||||
return menu;
|
||||
|
||||
if (menu.children) {
|
||||
const children = getCurrentMenu(menu.children, pathname);
|
||||
if (children) return children;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface MenuItem {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
export interface MenuService {
|
||||
getMenuRoles(siteId: string): Promise<MenuItem[]>;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Client } from "@common/network";
|
||||
import { BASE_URL } from "../config/api";
|
||||
import { SITE_ID } from "../config/env";
|
||||
import { setCookie } from "../lib";
|
||||
|
||||
setCookie("site-id-key", SITE_ID);
|
||||
|
||||
export const client = Client.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
siteId: SITE_ID,
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { client } from "./client";
|
||||
@@ -0,0 +1,6 @@
|
||||
export { useMutationWrapper } from "./use-mutation-wrapper";
|
||||
export { useAlertStore } from "./use-alert-store";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
export { usePrompt } from "./use-prompt";
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { AlertStore } from "../store/alert-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useAlertStore = <T>(selector: (store: AlertStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useAlertStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.alertStore, selector);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { MenuStore } from "../store/menu-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useMenuStore = <T>(selector: (store: MenuStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useMenuStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.menuStore, selector);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
|
||||
import { useAlertStore } from "./use-alert-store";
|
||||
|
||||
export const useMutationWrapper = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
TVariables = void,
|
||||
TContext = unknown,
|
||||
>(
|
||||
mutationOptions: UseMutationOptions<TData, TError, TVariables, TContext>,
|
||||
) => {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
return useMutation({
|
||||
...mutationOptions,
|
||||
onError: (error) => {
|
||||
let errorMessage = "";
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status)
|
||||
errorMessage += `status: ${error.response.status}\n`;
|
||||
if (error.response?.data?.message)
|
||||
errorMessage += `${error.response.data.message}\n`;
|
||||
if (error.response?.data?.errors[0]?.message)
|
||||
errorMessage += `${error.response.data.errors[0].message}\n`;
|
||||
}
|
||||
|
||||
alert(`[Error] ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useContext } from "react";
|
||||
import { PromptContext } from "../providers/prompt-provider";
|
||||
|
||||
export const usePrompt = () => {
|
||||
const { message, resolve, setMessage, setResolve } =
|
||||
useContext(PromptContext);
|
||||
|
||||
const prompt = (message: string) => {
|
||||
setMessage(message);
|
||||
return new Promise<{ resolved: boolean; value?: string }>((resolve) => {
|
||||
setResolve(() => resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (value: string) => {
|
||||
resolve?.({ resolved: true, value });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
resolve?.({ resolved: false });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
return { message, isOpen: !!message, prompt, onSubmit, onCancel };
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { UserStore } from "../store/user-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useUserStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.userStore, selector);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { StoreProvider } from "./store-provider";
|
||||
export { PromptProvider } from "./prompt-provider";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
interface PromptContext {
|
||||
message?: string;
|
||||
resolve?: ({
|
||||
resolved,
|
||||
value,
|
||||
}: {
|
||||
resolved: boolean;
|
||||
value?: string;
|
||||
}) => void;
|
||||
setMessage: (message: string) => void;
|
||||
setResolve: (resolve: () => void) => void;
|
||||
}
|
||||
|
||||
export const PromptContext = createContext<PromptContext>({
|
||||
message: "",
|
||||
resolve() {},
|
||||
setMessage() {},
|
||||
setResolve() {},
|
||||
});
|
||||
|
||||
export const PromptProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [message, setMessage] = useState<string>();
|
||||
const [resolve, setResolve] =
|
||||
useState<
|
||||
({ resolved, value }: { resolved: boolean; value?: string }) => void
|
||||
>();
|
||||
|
||||
return (
|
||||
<PromptContext.Provider
|
||||
value={{ message, resolve, setMessage, setResolve }}
|
||||
>
|
||||
{children}
|
||||
</PromptContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useRef } from "react";
|
||||
import {
|
||||
createAlertStore,
|
||||
createAttachmentStore,
|
||||
createMenuStore,
|
||||
createUserStore,
|
||||
} from "../store";
|
||||
|
||||
type StoreContextType = {
|
||||
alertStore: ReturnType<typeof createAlertStore>;
|
||||
attachmentStore: ReturnType<typeof createAttachmentStore>;
|
||||
menuStore: ReturnType<typeof createMenuStore>;
|
||||
userStore: ReturnType<typeof createUserStore>;
|
||||
};
|
||||
|
||||
export const StoreContext = createContext<StoreContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const storeRef = useRef<StoreContextType | null>(null);
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = {
|
||||
alertStore: createAlertStore(),
|
||||
attachmentStore: createAttachmentStore(),
|
||||
menuStore: createMenuStore(),
|
||||
userStore: createUserStore(),
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<StoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</StoreContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export type AlertState = {
|
||||
message: string;
|
||||
callback?: () => void;
|
||||
};
|
||||
|
||||
export type AlertActions = {
|
||||
setMessage: (message: string, callback?: () => void) => void;
|
||||
};
|
||||
|
||||
export type AlertStore = AlertState & AlertActions;
|
||||
|
||||
export const defaultInitState: AlertState = {
|
||||
message: "",
|
||||
callback: undefined,
|
||||
};
|
||||
|
||||
export const createAlertStore = (initState: AlertState = defaultInitState) => {
|
||||
return createStore<AlertStore>()((set) => ({
|
||||
...initState,
|
||||
setMessage: (message: string, callback?: () => void) =>
|
||||
set(() => ({ message, callback })),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export type AttachmentState = {
|
||||
attachmentFileCode: string;
|
||||
};
|
||||
|
||||
export type AttachmentActions = {
|
||||
setAttachmentFileCode: (attachmentFileCode: string) => void;
|
||||
};
|
||||
|
||||
export type AttachmentStore = AttachmentState & AttachmentActions;
|
||||
|
||||
export const defaultInitState: AttachmentState = {
|
||||
attachmentFileCode: "",
|
||||
};
|
||||
|
||||
export const createAttachmentStore = (
|
||||
initState: AttachmentState = defaultInitState,
|
||||
) => {
|
||||
return createStore<AttachmentStore>()((set) => ({
|
||||
...initState,
|
||||
setAttachmentFileCode: (attachmentFileCode: string) =>
|
||||
set({ attachmentFileCode }),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export { createAlertStore } from "./alert-store";
|
||||
export { createAttachmentStore } from "./attachment-store";
|
||||
export { createMenuStore, type BbsType } from "./menu-store";
|
||||
export { createUserStore, type IUserInfo } from "./user-store";
|
||||
@@ -0,0 +1,93 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export const BBS_TYPE = {
|
||||
BBSNOTICE: "BBSNOTICE",
|
||||
BBSNORMAL: "BBSNORMAL",
|
||||
BBSFAQ: "BBSFAQ",
|
||||
BBSIMAGE: "BBSIMAGE",
|
||||
BBSMOVIE: "BBSMOVIE",
|
||||
} as const;
|
||||
|
||||
export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE];
|
||||
|
||||
export interface IBbs {
|
||||
bbsId: number;
|
||||
bbsTypeCd: BbsType;
|
||||
bbsTypeNm: string;
|
||||
bbsNm: string;
|
||||
bbsExpln: string;
|
||||
rfrncUseYn: string;
|
||||
atflUseYn: string;
|
||||
atflMaxCnt: number;
|
||||
atflMaxFileSz: number;
|
||||
ansUseYn: string;
|
||||
ansUseAuthrt: string;
|
||||
cmntUseYn: string;
|
||||
dgstfnEvlUseYn: string;
|
||||
smsUseYn: string;
|
||||
upndFixUseYn: string;
|
||||
pstgPrdUseYn: string;
|
||||
popupUseYn: string;
|
||||
delYn: string;
|
||||
useYn: string;
|
||||
mdfcnDt: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
interface IMenu {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: IMenu[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
interface IAuthority {
|
||||
mngAuthrtYn: string;
|
||||
inqAuthrtYn: string;
|
||||
regAuthrtYn: string;
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
}
|
||||
|
||||
export type MenuState = {
|
||||
bbsInfo: IBbs | null;
|
||||
currentMenuInfo: IMenu | null;
|
||||
pageMenuAuthority: IAuthority | null;
|
||||
};
|
||||
|
||||
export type MenuActions = {
|
||||
setBbsInfo: (bbs: IBbs) => void;
|
||||
setCurrentMenuInfo: (menu: IMenu) => void;
|
||||
setPageMenuAuthority: (authority: IAuthority) => void;
|
||||
};
|
||||
|
||||
export type MenuStore = MenuState & MenuActions;
|
||||
|
||||
export const defaultInitState: MenuState = {
|
||||
currentMenuInfo: null,
|
||||
bbsInfo: null,
|
||||
pageMenuAuthority: null,
|
||||
};
|
||||
|
||||
export const createMenuStore = (initState: MenuState = defaultInitState) => {
|
||||
return createStore<MenuStore>()((set) => ({
|
||||
...initState,
|
||||
setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }),
|
||||
setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }),
|
||||
setPageMenuAuthority: (authority: IAuthority) =>
|
||||
set({ pageMenuAuthority: authority }),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export interface IUserInfo {
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgtr: string;
|
||||
mdfr: string;
|
||||
adminRoleYn: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
encryptedPassword: string;
|
||||
refreshToken: string;
|
||||
userStateCode: string;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
pswdHnt: string;
|
||||
pswdAns: string;
|
||||
mphn: string;
|
||||
telno: string;
|
||||
zip: string;
|
||||
addr: string;
|
||||
daddr: string;
|
||||
laeSeCd: string;
|
||||
ogdpSeCd: string;
|
||||
aprvYn: string;
|
||||
autzrId: string;
|
||||
expryYmd: string;
|
||||
lastPswdChgDt: string;
|
||||
googleId: string;
|
||||
kakaoId: string;
|
||||
naverId: string;
|
||||
isSocialUser: boolean;
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
export type UserState = {
|
||||
userInfo: IUserInfo | null;
|
||||
};
|
||||
|
||||
export type UserActions = {
|
||||
setUserInfo: (userInfo: IUserInfo | null) => void;
|
||||
};
|
||||
|
||||
export type UserStore = UserState & UserActions;
|
||||
|
||||
export const defaultInitState: UserState = {
|
||||
userInfo: null,
|
||||
};
|
||||
|
||||
export const createUserStore = (initState: UserState = defaultInitState) => {
|
||||
return createStore<UserStore>()((set) => ({
|
||||
...initState,
|
||||
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
} from "@common/components";
|
||||
import { useAlertStore } from "../lib/use-alert-store";
|
||||
|
||||
function AlertDialog() {
|
||||
const { message, callback, setMessage } = useAlertStore((state) => state);
|
||||
const handleOpenChange = () => {
|
||||
setMessage("");
|
||||
callback?.();
|
||||
};
|
||||
return (
|
||||
<Dialog open={!!message} onOpenChange={handleOpenChange}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button>확인</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AlertDialog };
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
type ButtonProps,
|
||||
} from "@common/components";
|
||||
|
||||
type ConfirmDialogProps = {
|
||||
isOpen?: boolean;
|
||||
confirmButtonProps?: ButtonProps & { label?: string };
|
||||
cancelButtonProps?: ButtonProps & { label?: string };
|
||||
};
|
||||
|
||||
function ConfirmDialog({
|
||||
isOpen,
|
||||
confirmButtonProps,
|
||||
cancelButtonProps,
|
||||
}: ConfirmDialogProps) {
|
||||
const { message, isOpen: isGlobalOpen, onConfirm, onCancel } = useConfirm();
|
||||
return (
|
||||
<Dialog open={isOpen ?? isGlobalOpen} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onConfirm} {...confirmButtonProps}>
|
||||
{confirmButtonProps?.label ?? "확인"}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onCancel} color="white" {...cancelButtonProps}>
|
||||
{cancelButtonProps?.label ?? "취소"}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ConfirmDialog };
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useMenuStore } from "../lib/use-menu-store";
|
||||
|
||||
function MapLayout({ children }: { children: React.ReactNode }) {
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MapLayout };
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PageTitle } from "@common/components";
|
||||
import { useMenuStore } from "../lib/use-menu-store";
|
||||
|
||||
function PageLayout({ children }: { children: React.ReactNode }) {
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
const menuPaths = currentMenuInfo?.menuPath
|
||||
?.split(">")
|
||||
.map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
<PageTitle
|
||||
title={currentMenuInfo?.menuKornNm ?? ""}
|
||||
menuPaths={menuPaths ?? []}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageLayout };
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { usePrompt } from "@/src/shared/lib/use-prompt";
|
||||
|
||||
function PromptDialog() {
|
||||
const { message, isOpen, onSubmit, onCancel } = usePrompt();
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="flex flex-col gap-6 pb-4 pt-8">
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
className="!text-[1.3rem]"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button onClick={() => onSubmit(value)}>확인</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onCancel} color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PromptDialog };
|
||||
Reference in New Issue
Block a user