@@ -154,7 +153,7 @@ const Page = () => {
{ await registFile(file, year, quater, cd, id) }} />
-
+ >
)
}
diff --git a/apps/sn/app/clientLayout.tsx b/apps/sn/app/clientLayout.tsx
deleted file mode 100644
index e8fdbe032..000000000
--- a/apps/sn/app/clientLayout.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-'use client'
-
-import { BaseLayout } from "@common/components";
-
-export function ClientLayout({ children }: { children: React.ReactNode }) {
- return
{children};
-}
\ No newline at end of file
diff --git a/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx b/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx
index bf8a70130..f487034d1 100644
--- a/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx
+++ b/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx
@@ -1,4 +1,4 @@
-import { getCookie } from "@utils";
+import { getCookie } from "@/components/lib/utils";
import { ENDPOINT } from "../api/attachment/attachment-service";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
diff --git a/apps/sn/app/components/lib/utils/fileUploadUtil.tsx b/apps/sn/app/components/lib/utils/fileUploadUtil.tsx
index 9b3721cd0..195a96b0d 100644
--- a/apps/sn/app/components/lib/utils/fileUploadUtil.tsx
+++ b/apps/sn/app/components/lib/utils/fileUploadUtil.tsx
@@ -1,5 +1,5 @@
import { Upload, type HttpResponse } from "tus-js-client";
-import { getCookie, setCookie } from "@utils";
+import { getCookie, setCookie } from "@/components/lib/utils";
import { ENDPOINT } from "../api/attachment/attachment-service";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
diff --git a/apps/sn/app/components/lib/utils/index.tsx b/apps/sn/app/components/lib/utils/index.tsx
index 0b9999c87..9a41c2740 100644
--- a/apps/sn/app/components/lib/utils/index.tsx
+++ b/apps/sn/app/components/lib/utils/index.tsx
@@ -1,4 +1,7 @@
-export { getCookie, setCookie, removeCookie } from "./cookie";
-export { handleUpload } from "./fileUploadUtil";
+export * from "./cookie";
+export * from "./fileUploadUtil";
export * from "./fileDownload";
export * from './date'
+export * from './excelToJson'
+export * from './fileDeleteUtil'
+export * from './fileDownload'
diff --git a/apps/sn/app/components/ui/fileUploadForm.tsx b/apps/sn/app/components/ui/fileUploadForm.tsx
index 0760b3252..7fcdeb353 100644
--- a/apps/sn/app/components/ui/fileUploadForm.tsx
+++ b/apps/sn/app/components/ui/fileUploadForm.tsx
@@ -1,7 +1,7 @@
"use client"
import { FileUploader } from "@common/components";
import { Button } from '@common/components/ui';
-import { handleUpload } from "@utils";
+import { handleUpload } from "@/components/lib/utils";
import { useId, useState } from 'react';
import type { HttpResponse, Upload } from "tus-js-client";
diff --git a/apps/sn/app/components/ui/operFileUploadForm.tsx b/apps/sn/app/components/ui/operFileUploadForm.tsx
index bd1c67996..c3aaa6ddd 100644
--- a/apps/sn/app/components/ui/operFileUploadForm.tsx
+++ b/apps/sn/app/components/ui/operFileUploadForm.tsx
@@ -2,7 +2,7 @@
import { SelectBox } from '@common/business_components/ui';
import React, { useState } from 'react'
import FileUploadForm from './fileUploadForm';
-import { yearOptions } from '../lib/utils';
+import { yearOptions } from '@/components/lib/utils';
const operFileUploadForm = ({ title, onSubmit }: { title: string, onSubmit: (files: string, year: string, quarter: string, code:string, id:string) => void }) => {
const [atflCd, setAtflCd] = useState
(null);
diff --git a/apps/sn/app/layout.tsx b/apps/sn/app/layout.tsx
index f983b20bc..066d9646b 100644
--- a/apps/sn/app/layout.tsx
+++ b/apps/sn/app/layout.tsx
@@ -1,10 +1,5 @@
-import "./styles/base.css";
-import "./styles/components.css";
import type { Metadata } from "next";
-import { cookies } from "next/headers";
-import { StoreProvider } from "./store/store-provider";
-import { AuthWrapper } from "./components/lib/auth";
-import { ClientLayout } from "./clientLayout";
+import { App } from "@/app";
export const metadata: Metadata = {
title: "디지털 하수관로",
@@ -16,22 +11,4 @@ export const metadata: Metadata = {
}
};
-export default async function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- const theme = (await cookies()).get("site-theme-key");
-
- return (
-
-
-
-
- {children}
-
-
-
-
- );
-}
+export default App;
\ No newline at end of file
diff --git a/apps/sn/app/page.tsx b/apps/sn/app/page.tsx
index 36295f554..846a46ad0 100644
--- a/apps/sn/app/page.tsx
+++ b/apps/sn/app/page.tsx
@@ -1,27 +1,46 @@
-'use client'
+"use client";
import Link from "next/link";
import { Button } from "@common/components/ui";
export default function Home() {
return (
-
-
운영정보 관리
+
+
하수관망도
-
-
+
+
+
-
I/I 분석(기초정보 설정)
+
+
운영정보 관리
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ I/I 분석(기초정보 설정)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/sn/app/service/index.ts b/apps/sn/app/service/index.ts
deleted file mode 100644
index c6bfcff72..000000000
--- a/apps/sn/app/service/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './map'
diff --git a/apps/sn/app/service/map.ts b/apps/sn/app/service/map.ts
deleted file mode 100644
index 2182e88c8..000000000
--- a/apps/sn/app/service/map.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-
-import axios from 'axios'
-
-/**
- * 관망도 API
- */
-const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`
-
-export type LyrStngProps = {
- lyr_sn: string
- lyr_nm: string
- upper_lyr_sn: string
- drawOrdr: string
- flterSql: string
- gobjTy: string
- lyrTpNm: string
- useYn: string
- minViewSize: string
- iconNm: string
- lblAtrbNm: string
- fontNm: string
- fontIntnColor: string
- fontExtnColor: string
- fontExtnWdth: string
- lnWdth: string
- lnColor: string
- polgnColor: string
- initEprssYn: string
- clickPsblYn: string
-}
-
-export const mapService = {
- //레이어 설정 조회
- getLyrStngs: () => {
- return axios.get(`${API_URL}/lyrStngs/SN_NETWORK`)
- }
-}
diff --git a/apps/sn/app/store/index.tsx b/apps/sn/app/store/index.tsx
deleted file mode 100644
index a9fc1288d..000000000
--- a/apps/sn/app/store/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { useUserStore, type IUserInfo } from "./user-store";
diff --git a/apps/sn/app/store/user-store.tsx b/apps/sn/app/store/user-store.tsx
deleted file mode 100644
index 8719dc842..000000000
--- a/apps/sn/app/store/user-store.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-"use client";
-
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
-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;
-};
-
-type UserStore = {
- userInfo: IUserInfo | null;
- setUserInfo: (userInfo: IUserInfo | null) => void;
-};
-
-// ✅ zustand 기본 create + persist 적용
-export const useUserStore = create
()(
- persist(
- (set) => ({
- userInfo: null,
- setUserInfo: (userInfo) => set({ userInfo }),
- }),
- {
- name: "user-store", // localStorage 키
- partialize: (state) => ({ userInfo: state.userInfo }), // 선택적 저장
- },
- ),
-);
-
-export const defaultInitState: UserState = {
- userInfo: null,
-};
-
-export const createUserStore = (initState: UserState = defaultInitState) => {
- return createStore()((set) => ({
- ...initState,
- setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
- }));
-};
diff --git a/apps/sn/package.json b/apps/sn/package.json
index 42c19afa7..caac2aadb 100644
--- a/apps/sn/package.json
+++ b/apps/sn/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev -p 3002",
+ "dev": "next dev -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint"
@@ -14,9 +14,11 @@
"@common/components": "workspace:*",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@hookform/resolvers": "^4.1.3",
+ "@minoru/react-dnd-treeview": "3.5.1-alpha.1",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "^5.67.1",
+ "@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
@@ -24,6 +26,7 @@
"js-cookie": "^3.0.5",
"next": "15.1.7",
"react-chartjs-2": "^5.3.0",
+ "react-dnd": "^16.0.1",
"react-hook-form": "^7.54.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2",
diff --git a/apps/sn/src/app/index.tsx b/apps/sn/src/app/index.tsx
new file mode 100644
index 000000000..692b55ef0
--- /dev/null
+++ b/apps/sn/src/app/index.tsx
@@ -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 (
+
+
+ {children}
+
+
+ );
+};
+
+export { App };
diff --git a/apps/sn/src/app/providers/app-provider.tsx b/apps/sn/src/app/providers/app-provider.tsx
new file mode 100644
index 000000000..81821a635
--- /dev/null
+++ b/apps/sn/src/app/providers/app-provider.tsx
@@ -0,0 +1,46 @@
+"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";
+
+
+const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
+ const queryClient = new QueryClient();
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { AppProvider };
diff --git a/apps/sn/src/app/providers/index.ts b/apps/sn/src/app/providers/index.ts
new file mode 100644
index 000000000..15f99b7fd
--- /dev/null
+++ b/apps/sn/src/app/providers/index.ts
@@ -0,0 +1 @@
+export { AppProvider } from "./app-provider";
diff --git a/apps/sn/app/styles/base.css b/apps/sn/src/app/styles/base.css
similarity index 100%
rename from apps/sn/app/styles/base.css
rename to apps/sn/src/app/styles/base.css
diff --git a/apps/sn/app/styles/components.css b/apps/sn/src/app/styles/components.css
similarity index 100%
rename from apps/sn/app/styles/components.css
rename to apps/sn/src/app/styles/components.css
diff --git a/apps/sn/app/components/lib/auth/api/login-service.tsx b/apps/sn/src/entities/auth/api/login-service.ts
similarity index 69%
rename from apps/sn/app/components/lib/auth/api/login-service.tsx
rename to apps/sn/src/entities/auth/api/login-service.ts
index 44b7da488..c1968fe36 100644
--- a/apps/sn/app/components/lib/auth/api/login-service.tsx
+++ b/apps/sn/src/entities/auth/api/login-service.ts
@@ -1,31 +1,14 @@
import { AxiosError } from "axios";
+import {
+ ENDPOINT,
+ JWT_EXPIRATION_TIME,
+ ACCESS_TOKEN,
+ AUTH_USER_ID,
+ CLAIM_NAME,
+} from "@/shared/config";
+import type { LoginService, ILogin } from "../model/login-interface";
import { tokenManager } from "../lib/token-manager";
-const ENDPOINT = {
- USER_SERVICE: {
- LOGIN: "/user-service/login",
- USERS: "/user-service/api/v1/users",
- SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
- AUTHORIZATIONS: "/user-service/api/v1/authorizations",
- ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations",
- },
-};
-const JWT_EXPIRATION_TIME = 1800000;
-const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
-const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
-const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
-
-
-export interface ILogin {
- userId: string;
- password: string;
-}
-
-export interface LoginService {
- login(payload: ILogin): Promise;
- silentRefresh(): Promise;
-}
-
export class LoginApiService implements LoginService {
private static instance: LoginApiService;
@@ -51,14 +34,13 @@ export class LoginApiService implements LoginService {
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
-
+
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
-
tokenManager.setupRefreshTimer(
() => this.silentRefresh(),
JWT_EXPIRATION_TIME,
@@ -75,14 +57,12 @@ export class LoginApiService implements LoginService {
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
- {
- method: "PUT",
- },
+ { method: "PUT" },
);
const data = await response.json();
- const accessToken = response.headers.get("access-token");
- const tokenId = response.headers.get("token-id");
+ const accessToken = data[ACCESS_TOKEN];
+ const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
diff --git a/apps/sn/app/components/lib/auth/index.tsx b/apps/sn/src/entities/auth/index.ts
similarity index 100%
rename from apps/sn/app/components/lib/auth/index.tsx
rename to apps/sn/src/entities/auth/index.ts
diff --git a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx
similarity index 72%
rename from apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx
rename to apps/sn/src/entities/auth/lib/auth-wrapper.tsx
index 0b72cc768..c09ca55c6 100644
--- a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx
+++ b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx
@@ -1,30 +1,20 @@
"use client";
import { useEffect } from "react";
-import { useRouter, usePathname } from "next/navigation";
+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 "@/components/lib/utils";
import axios from "axios";
-import Cookies from "js-cookie";
-import { useUserStore, type IUserInfo } from "@store";
-import { getCookie } from "@utils";
import { LoginApi } from "../api/login-service";
import { tokenManager } from "../lib/token-manager";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
-const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
-
-const ENDPOINT = {
- USER_SERVICE: {
- LOGIN: "/user-service/login",
- USERS: "/user-service/api/v1/users",
- SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
- AUTHORIZATIONS: "/user-service/api/v1/authorizations",
- ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations",
- },
-};
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
- const pathname = usePathname(); // 현재 경로
const { userInfo, setUserInfo } = useUserStore((state) => state);
async function authCheck() {
@@ -62,7 +52,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
try {
await authCheck();
} catch (error) {
- console.error("Auth check failed:", error);
+ console.error(error);
try {
await LoginApi.silentRefresh();
@@ -70,7 +60,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
setUserInfo(null);
-
+
// TODO 운영에 반영 할때 수정해야.....
// 현재 경로가 로그인 페이지가 아닐 때만 redirect
// if (!pathname.startsWith("/auth/login")) {
@@ -79,7 +69,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
}
}
})();
- }, [pathname]); // pathname도 의존성에 포함
+ }, []);
return <>{children}>;
};
diff --git a/apps/sn/app/components/lib/auth/lib/token-manager.tsx b/apps/sn/src/entities/auth/lib/token-manager.ts
similarity index 86%
rename from apps/sn/app/components/lib/auth/lib/token-manager.tsx
rename to apps/sn/src/entities/auth/lib/token-manager.ts
index 9feab4af0..bf3d68265 100644
--- a/apps/sn/app/components/lib/auth/lib/token-manager.tsx
+++ b/apps/sn/src/entities/auth/lib/token-manager.ts
@@ -1,7 +1,5 @@
-import { getCookie, setCookie, removeCookie } from "@utils";
-
-const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
-const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
+import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
+import { getCookie, setCookie, removeCookie } from "@/components/lib/utils";
export class TokenManager {
private static instance: TokenManager;
diff --git a/apps/sn/app/components/lib/auth/model/login-interface.tsx b/apps/sn/src/entities/auth/model/login-interface.ts
similarity index 100%
rename from apps/sn/app/components/lib/auth/model/login-interface.tsx
rename to apps/sn/src/entities/auth/model/login-interface.ts
diff --git a/apps/sn/src/entities/menu-authority/@x/menu.ts b/apps/sn/src/entities/menu-authority/@x/menu.ts
new file mode 100644
index 000000000..86479f859
--- /dev/null
+++ b/apps/sn/src/entities/menu-authority/@x/menu.ts
@@ -0,0 +1 @@
+export { MenuAuthorityApi } from "../api/menu-authority-service";
diff --git a/apps/sn/src/entities/menu-authority/@x/posting.ts b/apps/sn/src/entities/menu-authority/@x/posting.ts
new file mode 100644
index 000000000..bfe84fa0d
--- /dev/null
+++ b/apps/sn/src/entities/menu-authority/@x/posting.ts
@@ -0,0 +1 @@
+export { AuthorityButton } from "../ui/authority-button";
diff --git a/apps/sn/src/entities/menu-authority/api/menu-authority-service.ts b/apps/sn/src/entities/menu-authority/api/menu-authority-service.ts
new file mode 100644
index 000000000..14988eee0
--- /dev/null
+++ b/apps/sn/src/entities/menu-authority/api/menu-authority-service.ts
@@ -0,0 +1,40 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/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 {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async getPageMenuAuthority(
+ menuCd: number,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`,
+ );
+ return response.data;
+ }
+
+ public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise {
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
+ data,
+ );
+ return response.data;
+ }
+}
+
+export const MenuAuthorityApi = new MenuAuthorityApiService();
diff --git a/apps/sn/src/entities/menu-authority/index.ts b/apps/sn/src/entities/menu-authority/index.ts
new file mode 100644
index 000000000..2a555b77e
--- /dev/null
+++ b/apps/sn/src/entities/menu-authority/index.ts
@@ -0,0 +1,2 @@
+export { AuthorityButton } from "./ui/authority-button";
+export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
diff --git a/apps/sn/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/sn/src/entities/menu-authority/model/menu-authority-interface.ts
new file mode 100644
index 000000000..b59574c2f
--- /dev/null
+++ b/apps/sn/src/entities/menu-authority/model/menu-authority-interface.ts
@@ -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;
+export type MenuAuthorityList = PageableList;
+export type MenuAuthorityPayload = z.infer;
+export type MenuAuthorityFormData = z.infer;
+
+export interface MenuAuthorityService {
+ getMenuAuthorityList(params: MenuAuthorityParams): Promise;
+ getPageMenuAuthority(menuCd: number): Promise;
+ saveMenuAuthority(data: MenuAuthorityPayload[]): Promise;
+}
diff --git a/apps/sn/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/sn/src/entities/menu-authority/model/menu-authority-table-columns.tsx
new file mode 100644
index 000000000..6969a85e9
--- /dev/null
+++ b/apps/sn/src/entities/menu-authority/model/menu-authority-table-columns.tsx
@@ -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,
+): ColumnDef[] => {
+ 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 (
+ {
+ 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 (
+ {
+ if (checked) {
+ handleCheckedChange(
+ row.index,
+ "inqAuthrtYn",
+ )(checked as boolean);
+ }
+ handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean);
+ }}
+ />
+ );
+ },
+ },
+ {
+ accessorKey: "mdfcnAuthrtYn",
+ header: "수정권한 ",
+ cell: ({ row }) => {
+ return (
+
+ );
+ },
+ },
+ {
+ accessorKey: "delAuthrtYn",
+ header: "삭제권한",
+ cell: ({ row }) => {
+ return (
+
+ );
+ },
+ },
+ {
+ accessorKey: "inqAuthrtYn",
+ header: "조회권한",
+ cell: ({ row }) => {
+ return (
+
+ );
+ },
+ },
+ ];
+};
diff --git a/apps/sn/src/entities/menu-authority/ui/authority-button.tsx b/apps/sn/src/entities/menu-authority/ui/authority-button.tsx
new file mode 100644
index 000000000..257ab6e5d
--- /dev/null
+++ b/apps/sn/src/entities/menu-authority/ui/authority-button.tsx
@@ -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 (
+
+ );
+}
+
+export { AuthorityButton };
diff --git a/apps/sn/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/sn/src/entities/menu-authority/ui/management-authority-dialog.tsx
new file mode 100644
index 000000000..4a73cb596
--- /dev/null
+++ b/apps/sn/src/entities/menu-authority/ui/management-authority-dialog.tsx
@@ -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;
+
+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({
+ 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 (
+
+ );
+}
+
+export { ManagementAuthorityDialog };
diff --git a/apps/sn/src/entities/menu/api/menu-service.ts b/apps/sn/src/entities/menu/api/menu-service.ts
new file mode 100644
index 000000000..4424f8d37
--- /dev/null
+++ b/apps/sn/src/entities/menu/api/menu-service.ts
@@ -0,0 +1,14 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+import type { MenuItem, MenuService } from "../model/menu-interface";
+
+export class MenuApiService implements MenuService {
+ async getMenuRoles(siteId: string): Promise