diff --git a/apps/wr/app/layout.tsx b/apps/wr/app/layout.tsx
index f728f5de0..25530bfef 100644
--- a/apps/wr/app/layout.tsx
+++ b/apps/wr/app/layout.tsx
@@ -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 (
-
-
-
-
- {children}
-
-
-
-
- );
-}
+export default App;
\ No newline at end of file
diff --git a/apps/wr/app/page.tsx b/apps/wr/app/page.tsx
index 8729ac5f0..37fbe8b00 100644
--- a/apps/wr/app/page.tsx
+++ b/apps/wr/app/page.tsx
@@ -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 (
- <>
-
- {/* 시설 등록 */}
-
-
시설 등록
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ const userId = "CLEANWATER";
+ const password = "gktneh12!@";
+ const checkToken = getCookie(AUTH_USER_ID);
- {/* 지도점검 결과 */}
-
-
지도점검 결과
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* 시설 현황 */}
-
-
시설 현황
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* 수요처 관리 */}
-
-
수요처 관리
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* 수요처 매칭 */}
-
-
수요처 매칭
-
-
-
-
-
-
-
-
-
-
-
-
- {/* 통계*/}
-
-
통계 매칭
-
-
-
-
-
-
-
-
-
-
- {/* 시설 현황(지도) */}
-
-
시설 현황(지도)
-
-
-
-
-
-
-
- >
- );
+ 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");
+ }
+ }, []);
}
diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts
index 5dc77eab4..0a3658a0b 100644
--- a/apps/wr/middleware.ts
+++ b/apps/wr/middleware.ts
@@ -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;
diff --git a/apps/wr/src/app/index.tsx b/apps/wr/src/app/index.tsx
new file mode 100644
index 000000000..692b55ef0
--- /dev/null
+++ b/apps/wr/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/wr/src/app/providers/app-provider.tsx b/apps/wr/src/app/providers/app-provider.tsx
new file mode 100644
index 000000000..b8eb35ec6
--- /dev/null
+++ b/apps/wr/src/app/providers/app-provider.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { AppProvider };
diff --git a/apps/wr/src/app/providers/index.ts b/apps/wr/src/app/providers/index.ts
new file mode 100644
index 000000000..15f99b7fd
--- /dev/null
+++ b/apps/wr/src/app/providers/index.ts
@@ -0,0 +1 @@
+export { AppProvider } from "./app-provider";
diff --git a/apps/wr/src/app/styles/base.css b/apps/wr/src/app/styles/base.css
new file mode 100644
index 000000000..8453ff9e8
--- /dev/null
+++ b/apps/wr/src/app/styles/base.css
@@ -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;
+}
diff --git a/apps/wr/src/app/styles/components.css b/apps/wr/src/app/styles/components.css
new file mode 100644
index 000000000..4dc30bafd
--- /dev/null
+++ b/apps/wr/src/app/styles/components.css
@@ -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;
+ }
+}
diff --git a/apps/wr/src/entities/auth/index.ts b/apps/wr/src/entities/auth/index.ts
index 9dc9dd368..97e142225 100644
--- a/apps/wr/src/entities/auth/index.ts
+++ b/apps/wr/src/entities/auth/index.ts
@@ -1,2 +1,2 @@
export { LoginApi } from "./api/login-service";
-//export { AuthWrapper } from "./lib/auth-wrapper";
+export { AuthWrapper } from "./lib/auth-wrapper";
diff --git a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx
new file mode 100644
index 000000000..deee0e8fe
--- /dev/null
+++ b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx
@@ -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(
+ `${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 };
diff --git a/apps/wr/src/entities/menu-authority/@x/menu.ts b/apps/wr/src/entities/menu-authority/@x/menu.ts
new file mode 100644
index 000000000..86479f859
--- /dev/null
+++ b/apps/wr/src/entities/menu-authority/@x/menu.ts
@@ -0,0 +1 @@
+export { MenuAuthorityApi } from "../api/menu-authority-service";
diff --git a/apps/wr/src/entities/menu-authority/@x/posting.ts b/apps/wr/src/entities/menu-authority/@x/posting.ts
new file mode 100644
index 000000000..bfe84fa0d
--- /dev/null
+++ b/apps/wr/src/entities/menu-authority/@x/posting.ts
@@ -0,0 +1 @@
+export { AuthorityButton } from "../ui/authority-button";
diff --git a/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts b/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts
new file mode 100644
index 000000000..c7ac056f4
--- /dev/null
+++ b/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts
@@ -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 {
+ 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/wr/src/entities/menu-authority/index.ts b/apps/wr/src/entities/menu-authority/index.ts
new file mode 100644
index 000000000..2a555b77e
--- /dev/null
+++ b/apps/wr/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/wr/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/wr/src/entities/menu-authority/model/menu-authority-interface.ts
new file mode 100644
index 000000000..b59574c2f
--- /dev/null
+++ b/apps/wr/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/wr/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/wr/src/entities/menu-authority/model/menu-authority-table-columns.tsx
new file mode 100644
index 000000000..6969a85e9
--- /dev/null
+++ b/apps/wr/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/wr/src/entities/menu-authority/ui/authority-button.tsx b/apps/wr/src/entities/menu-authority/ui/authority-button.tsx
new file mode 100644
index 000000000..257ab6e5d
--- /dev/null
+++ b/apps/wr/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/wr/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/wr/src/entities/menu-authority/ui/management-authority-dialog.tsx
new file mode 100644
index 000000000..4a73cb596
--- /dev/null
+++ b/apps/wr/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/wr/src/entities/menu/api/menu-service.ts b/apps/wr/src/entities/menu/api/menu-service.ts
new file mode 100644
index 000000000..8b08d7fa5
--- /dev/null
+++ b/apps/wr/src/entities/menu/api/menu-service.ts
@@ -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