From deea89fbb2a76b14068c008c517666845c2de953 Mon Sep 17 00:00:00 2001 From: yschoi Date: Wed, 18 Jun 2025 17:56:20 +0900 Subject: [PATCH] =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A9=94=EB=89=B4=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/(map-layout)/layout.tsx | 34 +++++ .../{ => (map-layout)}/map/SearchModal.tsx | 0 .../map/components/InputSection.tsx | 0 .../map/components/common/index.ts | 0 .../map/components/common/table.ts | 0 .../components/popupComponent/MsSwagePpl.tsx | 0 .../map/components/popupComponent/Ssc.tsx | 0 .../components/popupComponent/SwtrmtFclt.tsx | 0 .../popupComponent/chart/infilChart.tsx | 0 .../components/popupComponent/sewagePump.tsx | 0 .../map/components/popupComponent/smh.tsx | 0 .../popupComponent/stormWaterPump.tsx | 0 .../map/featureClickEventPopup.tsx | 0 apps/sn/app/{ => (map-layout)}/map/page.tsx | 33 ++++- .../ii/BaseLayout.tsx | 2 - .../ii/irsetting/page.tsx | 2 +- .../{ => (with-base-layout)}/ii/rdii/page.tsx | 6 +- .../ii/rdii_result/page.tsx | 2 +- .../ii/result/page.tsx | 2 +- apps/sn/app/(with-base-layout)/layout.tsx | 35 ++++++ .../oper/floodPre/page.tsx | 5 +- .../{ => (with-base-layout)}/oper/index.ts | 0 .../oper/oldPp/page.tsx | 9 +- apps/sn/app/clientLayout.tsx | 7 -- .../components/lib/utils/fileDeleteUtil.tsx | 2 +- .../components/lib/utils/fileUploadUtil.tsx | 2 +- apps/sn/app/components/lib/utils/index.tsx | 7 +- apps/sn/app/components/ui/fileUploadForm.tsx | 2 +- .../app/components/ui/operFileUploadForm.tsx | 2 +- apps/sn/app/layout.tsx | 27 +--- apps/sn/app/page.tsx | 47 ++++--- apps/sn/app/service/index.ts | 1 - apps/sn/app/service/map.ts | 37 ------ apps/sn/app/store/index.tsx | 1 - apps/sn/app/store/user-store.tsx | 74 ----------- apps/sn/package.json | 5 +- apps/sn/src/app/index.tsx | 20 +++ apps/sn/src/app/providers/app-provider.tsx | 46 +++++++ apps/sn/src/app/providers/index.ts | 1 + apps/sn/{ => src}/app/styles/base.css | 0 apps/sn/{ => src}/app/styles/components.css | 0 .../entities/auth/api/login-service.ts} | 44 ++----- .../index.tsx => src/entities/auth/index.ts} | 0 .../entities}/auth/lib/auth-wrapper.tsx | 28 ++--- .../entities/auth/lib/token-manager.ts} | 6 +- .../entities/auth/model/login-interface.ts} | 0 .../sn/src/entities/menu-authority/@x/menu.ts | 1 + .../src/entities/menu-authority/@x/posting.ts | 1 + .../api/menu-authority-service.ts | 40 ++++++ apps/sn/src/entities/menu-authority/index.ts | 2 + .../model/menu-authority-interface.ts | 48 +++++++ .../model/menu-authority-table-columns.tsx | 117 ++++++++++++++++++ .../menu-authority/ui/authority-button.tsx | 26 ++++ .../ui/management-authority-dialog.tsx | 103 +++++++++++++++ apps/sn/src/entities/menu/api/menu-service.ts | 14 +++ apps/sn/src/entities/menu/index.ts | 2 + .../sn/src/entities/menu/lib/menu-wrapper.tsx | 59 +++++++++ apps/sn/src/entities/menu/lib/utils.ts | 20 +++ .../src/entities/menu/model/menu-interface.ts | 23 ++++ apps/sn/src/shared/api/client.ts | 8 ++ apps/sn/src/shared/api/index.ts | 1 + apps/sn/src/shared/config/api.ts | 56 +++++++++ apps/sn/src/shared/config/env.ts | 11 ++ apps/sn/src/shared/config/index.ts | 2 + apps/sn/src/shared/lib/index.ts | 5 + apps/sn/src/shared/lib/use-alert-store.ts | 14 +++ apps/sn/src/shared/lib/use-menu-store.ts | 14 +++ .../src/shared/lib/use-mutation-wrapper.tsx | 31 +++++ apps/sn/src/shared/lib/use-prompt.tsx | 26 ++++ apps/sn/src/shared/lib/use-user-store.ts | 14 +++ apps/sn/src/shared/providers/index.ts | 2 + .../src/shared/providers/prompt-provider.tsx | 37 ++++++ .../shared/providers}/store-provider.tsx | 13 +- apps/sn/src/shared/store/alert-store.ts | 25 ++++ apps/sn/src/shared/store/attachment-store.ts | 25 ++++ apps/sn/src/shared/store/index.ts | 4 + apps/sn/src/shared/store/menu-store.ts | 93 ++++++++++++++ apps/sn/src/shared/store/user-store.ts | 57 +++++++++ apps/sn/src/shared/ui/alert-dialog.tsx | 34 +++++ apps/sn/src/shared/ui/confirm-dialog.tsx | 48 +++++++ apps/sn/src/shared/ui/map-layout.tsx | 13 ++ apps/sn/src/shared/ui/page-layout.tsx | 22 ++++ apps/sn/src/shared/ui/prompt-dialog.tsx | 45 +++++++ apps/sn/tsconfig.json | 16 ++- pnpm-lock.yaml | 9 ++ 85 files changed, 1318 insertions(+), 252 deletions(-) create mode 100644 apps/sn/app/(map-layout)/layout.tsx rename apps/sn/app/{ => (map-layout)}/map/SearchModal.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/components/InputSection.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/components/common/index.ts (100%) rename apps/sn/app/{ => (map-layout)}/map/components/common/table.ts (100%) rename apps/sn/app/{ => (map-layout)}/map/components/popupComponent/MsSwagePpl.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/components/popupComponent/Ssc.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/components/popupComponent/SwtrmtFclt.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/components/popupComponent/chart/infilChart.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/components/popupComponent/sewagePump.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/components/popupComponent/smh.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/components/popupComponent/stormWaterPump.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/featureClickEventPopup.tsx (100%) rename apps/sn/app/{ => (map-layout)}/map/page.tsx (84%) rename apps/sn/app/{ => (with-base-layout)}/ii/BaseLayout.tsx (98%) rename apps/sn/app/{ => (with-base-layout)}/ii/irsetting/page.tsx (99%) rename apps/sn/app/{ => (with-base-layout)}/ii/rdii/page.tsx (99%) rename apps/sn/app/{ => (with-base-layout)}/ii/rdii_result/page.tsx (99%) rename apps/sn/app/{ => (with-base-layout)}/ii/result/page.tsx (99%) create mode 100644 apps/sn/app/(with-base-layout)/layout.tsx rename apps/sn/app/{ => (with-base-layout)}/oper/floodPre/page.tsx (97%) rename apps/sn/app/{ => (with-base-layout)}/oper/index.ts (100%) rename apps/sn/app/{ => (with-base-layout)}/oper/oldPp/page.tsx (96%) delete mode 100644 apps/sn/app/clientLayout.tsx delete mode 100644 apps/sn/app/service/index.ts delete mode 100644 apps/sn/app/service/map.ts delete mode 100644 apps/sn/app/store/index.tsx delete mode 100644 apps/sn/app/store/user-store.tsx create mode 100644 apps/sn/src/app/index.tsx create mode 100644 apps/sn/src/app/providers/app-provider.tsx create mode 100644 apps/sn/src/app/providers/index.ts rename apps/sn/{ => src}/app/styles/base.css (100%) rename apps/sn/{ => src}/app/styles/components.css (100%) rename apps/sn/{app/components/lib/auth/api/login-service.tsx => src/entities/auth/api/login-service.ts} (69%) rename apps/sn/{app/components/lib/auth/index.tsx => src/entities/auth/index.ts} (100%) rename apps/sn/{app/components/lib => src/entities}/auth/lib/auth-wrapper.tsx (72%) rename apps/sn/{app/components/lib/auth/lib/token-manager.tsx => src/entities/auth/lib/token-manager.ts} (86%) rename apps/sn/{app/components/lib/auth/model/login-interface.tsx => src/entities/auth/model/login-interface.ts} (100%) create mode 100644 apps/sn/src/entities/menu-authority/@x/menu.ts create mode 100644 apps/sn/src/entities/menu-authority/@x/posting.ts create mode 100644 apps/sn/src/entities/menu-authority/api/menu-authority-service.ts create mode 100644 apps/sn/src/entities/menu-authority/index.ts create mode 100644 apps/sn/src/entities/menu-authority/model/menu-authority-interface.ts create mode 100644 apps/sn/src/entities/menu-authority/model/menu-authority-table-columns.tsx create mode 100644 apps/sn/src/entities/menu-authority/ui/authority-button.tsx create mode 100644 apps/sn/src/entities/menu-authority/ui/management-authority-dialog.tsx create mode 100644 apps/sn/src/entities/menu/api/menu-service.ts create mode 100644 apps/sn/src/entities/menu/index.ts create mode 100644 apps/sn/src/entities/menu/lib/menu-wrapper.tsx create mode 100644 apps/sn/src/entities/menu/lib/utils.ts create mode 100644 apps/sn/src/entities/menu/model/menu-interface.ts create mode 100644 apps/sn/src/shared/api/client.ts create mode 100644 apps/sn/src/shared/api/index.ts create mode 100644 apps/sn/src/shared/config/api.ts create mode 100644 apps/sn/src/shared/config/env.ts create mode 100644 apps/sn/src/shared/config/index.ts create mode 100644 apps/sn/src/shared/lib/index.ts create mode 100644 apps/sn/src/shared/lib/use-alert-store.ts create mode 100644 apps/sn/src/shared/lib/use-menu-store.ts create mode 100644 apps/sn/src/shared/lib/use-mutation-wrapper.tsx create mode 100644 apps/sn/src/shared/lib/use-prompt.tsx create mode 100644 apps/sn/src/shared/lib/use-user-store.ts create mode 100644 apps/sn/src/shared/providers/index.ts create mode 100644 apps/sn/src/shared/providers/prompt-provider.tsx rename apps/sn/{app/store => src/shared/providers}/store-provider.tsx (60%) create mode 100644 apps/sn/src/shared/store/alert-store.ts create mode 100644 apps/sn/src/shared/store/attachment-store.ts create mode 100644 apps/sn/src/shared/store/index.ts create mode 100644 apps/sn/src/shared/store/menu-store.ts create mode 100644 apps/sn/src/shared/store/user-store.ts create mode 100644 apps/sn/src/shared/ui/alert-dialog.tsx create mode 100644 apps/sn/src/shared/ui/confirm-dialog.tsx create mode 100644 apps/sn/src/shared/ui/map-layout.tsx create mode 100644 apps/sn/src/shared/ui/page-layout.tsx create mode 100644 apps/sn/src/shared/ui/prompt-dialog.tsx diff --git a/apps/sn/app/(map-layout)/layout.tsx b/apps/sn/app/(map-layout)/layout.tsx new file mode 100644 index 000000000..e245cfdf1 --- /dev/null +++ b/apps/sn/app/(map-layout)/layout.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/entities/menu"; +import { useMenuStore } from "@/shared/lib"; + +export default function MapLayout({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + const pathname = usePathname(); + const queryClient = useQueryClient(); + + const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); + + const handleMenuClick = (item: MenuItem) => { + setCurrentMenuInfo(item); + router.push(item.urlPath); + }; + + return ( + + {children} + + ); +} diff --git a/apps/sn/app/map/SearchModal.tsx b/apps/sn/app/(map-layout)/map/SearchModal.tsx similarity index 100% rename from apps/sn/app/map/SearchModal.tsx rename to apps/sn/app/(map-layout)/map/SearchModal.tsx diff --git a/apps/sn/app/map/components/InputSection.tsx b/apps/sn/app/(map-layout)/map/components/InputSection.tsx similarity index 100% rename from apps/sn/app/map/components/InputSection.tsx rename to apps/sn/app/(map-layout)/map/components/InputSection.tsx diff --git a/apps/sn/app/map/components/common/index.ts b/apps/sn/app/(map-layout)/map/components/common/index.ts similarity index 100% rename from apps/sn/app/map/components/common/index.ts rename to apps/sn/app/(map-layout)/map/components/common/index.ts diff --git a/apps/sn/app/map/components/common/table.ts b/apps/sn/app/(map-layout)/map/components/common/table.ts similarity index 100% rename from apps/sn/app/map/components/common/table.ts rename to apps/sn/app/(map-layout)/map/components/common/table.ts diff --git a/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx similarity index 100% rename from apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx rename to apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx diff --git a/apps/sn/app/map/components/popupComponent/Ssc.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx similarity index 100% rename from apps/sn/app/map/components/popupComponent/Ssc.tsx rename to apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx diff --git a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx similarity index 100% rename from apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx rename to apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx diff --git a/apps/sn/app/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx similarity index 100% rename from apps/sn/app/map/components/popupComponent/chart/infilChart.tsx rename to apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx diff --git a/apps/sn/app/map/components/popupComponent/sewagePump.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx similarity index 100% rename from apps/sn/app/map/components/popupComponent/sewagePump.tsx rename to apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx diff --git a/apps/sn/app/map/components/popupComponent/smh.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx similarity index 100% rename from apps/sn/app/map/components/popupComponent/smh.tsx rename to apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx diff --git a/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/stormWaterPump.tsx similarity index 100% rename from apps/sn/app/map/components/popupComponent/stormWaterPump.tsx rename to apps/sn/app/(map-layout)/map/components/popupComponent/stormWaterPump.tsx diff --git a/apps/sn/app/map/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx similarity index 100% rename from apps/sn/app/map/featureClickEventPopup.tsx rename to apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx similarity index 84% rename from apps/sn/app/map/page.tsx rename to apps/sn/app/(map-layout)/map/page.tsx index e1a90d5f1..9b42a26f9 100644 --- a/apps/sn/app/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -1,12 +1,36 @@ "use client"; import Gis, { useGisContext } from "@common/business_components/ui/map/Gis"; -import { mapService } from "@service"; +import { getData } from "../../components/lib/api"; import { useCallback, useEffect, useRef, useState } from "react"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; import SearchModal from "./SearchModal"; import FeatureClickEventPopup from "./featureClickEventPopup"; +// 타입 정의 추가 +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; +}; + const Map = () => { const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 @@ -14,20 +38,19 @@ const Map = () => { const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const fetchData = async () => { try { - const response = await mapService.getLyrStngs(); - setTreeData(response.data); // API 응답 저장 + const data = await getData('v1/map/lyrStngs/SN_NETWORK'); + setTreeData(data); } catch (error) { console.error("데이터 불러오기 실패:", error); } }; - // 컴포넌트 마운트 시 1회 실행 useEffect(() => { fetchData(); }, []); - // SearchModal, GisContent 등에서 호출 const onFeatureSelect = (data: any) => { let map = gisMaps.current; diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx similarity index 98% rename from apps/sn/app/ii/BaseLayout.tsx rename to apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx index 544802744..6b16ad36a 100644 --- a/apps/sn/app/ii/BaseLayout.tsx +++ b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx @@ -202,7 +202,6 @@ const BaseLayout = ({ return (
- {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} {selectBox?.title && ( @@ -268,7 +267,6 @@ const BaseLayout = ({
{children?.[1]}
: null } -
) } diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx similarity index 99% rename from apps/sn/app/ii/irsetting/page.tsx rename to apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx index 2944af9c0..79facb5ef 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx @@ -9,7 +9,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' -import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData } from '../../components/lib/utils/excelToJson' +import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData } from '@/components/lib/utils' import BaseLayout from '../BaseLayout' const formSchema = z.object({ diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx similarity index 99% rename from apps/sn/app/ii/rdii/page.tsx rename to apps/sn/app/(with-base-layout)/ii/rdii/page.tsx index 2f61b57b9..116d87462 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx @@ -3,7 +3,7 @@ import { postData } from '@/components/lib/api' import FileUploadForm from "@/components/ui/fileUploadForm" import FormSection from "@/components/ui/formSection" -import BaseLayout from "@/ii/BaseLayout" +import BaseLayout from "../BaseLayout" import { DatePicker, filePicked } from "@common/business_components/ui" import { Input } from "@common/components/ui" import { zodResolver } from "@hookform/resolvers/zod" @@ -11,8 +11,8 @@ import { Label } from '@radix-ui/react-label' import { useState } from "react" import { useFieldArray, useForm } from "react-hook-form" import { z } from "zod" -import { excelToMatchingResultData, excelToRainData } from '../../components/lib/utils/excelToJson' -import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/EditableFormTable' +import { excelToMatchingResultData, excelToRainData } from '@/components/lib/utils' +import { EditableFormTable, type EditableTableHeadItem } from '../../../components/ui/EditableFormTable' // zod 스키마 정의 diff --git a/apps/sn/app/ii/rdii_result/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdii_result/page.tsx similarity index 99% rename from apps/sn/app/ii/rdii_result/page.tsx rename to apps/sn/app/(with-base-layout)/ii/rdii_result/page.tsx index 130c25419..b37a06dad 100644 --- a/apps/sn/app/ii/rdii_result/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdii_result/page.tsx @@ -6,7 +6,7 @@ import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import BaseLayout from '../BaseLayout' -import { handleDelete } from '@/components/lib/utils/fileDeleteUtil' +import { handleDelete } from '@/components/lib/utils' const formSchema = z.object({ diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/(with-base-layout)/ii/result/page.tsx similarity index 99% rename from apps/sn/app/ii/result/page.tsx rename to apps/sn/app/(with-base-layout)/ii/result/page.tsx index e2015c8d9..4759a2877 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/result/page.tsx @@ -6,7 +6,7 @@ import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import BaseLayout from '../BaseLayout' -import { handleDelete } from '@/components/lib/utils/fileDeleteUtil' +import { handleDelete } from '@/components/lib/utils' const formSchema = z.object({ diff --git a/apps/sn/app/(with-base-layout)/layout.tsx b/apps/sn/app/(with-base-layout)/layout.tsx new file mode 100644 index 000000000..3c1a527d7 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/layout.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/entities/menu"; +import { useMenuStore } from "@/shared/lib"; +import { PageLayout } from "@/shared/ui/page-layout"; + +export default function WithBaseLayout({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + const pathname = usePathname(); + const queryClient = useQueryClient(); + + const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); + + const handleMenuClick = (item: MenuItem) => { + setCurrentMenuInfo(item); + router.push(item.urlPath); + }; + + return ( + + {children} + + ); +} diff --git a/apps/sn/app/oper/floodPre/page.tsx b/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx similarity index 97% rename from apps/sn/app/oper/floodPre/page.tsx rename to apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx index 013b1a36c..000389a3e 100644 --- a/apps/sn/app/oper/floodPre/page.tsx +++ b/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx @@ -2,13 +2,12 @@ import { SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, UiTable } from '@common/business_components/ui' import { Button, DatePicker, Input, Label, LoadingWithAnimation } from '@common/components/ui' import React, { useEffect, useState } from 'react' -import OperFileUploadForm from '../../components/ui/operFileUploadForm' +import OperFileUploadForm from '../../../components/ui/operFileUploadForm' import ListDiv from '@common/business_components/ui/list_div' import ContentLayout from '@/components/ui/contentLayout' import { searchCmmnCd, searchData } from '@/components/lib/common/apiHelpers' import { tableHeadlist } from '..' -import { handleDownload } from '@/components/lib/utils/fileDownload' -import { formatDate, yearOptions } from '@/components/lib/utils/date' +import { formatDate, yearOptions, handleDownload } from '@/components/lib/utils' import { deleteData, postData } from '@/components/lib/api' import LoadingOverlay from '@/components/ui/LoadingBar' diff --git a/apps/sn/app/oper/index.ts b/apps/sn/app/(with-base-layout)/oper/index.ts similarity index 100% rename from apps/sn/app/oper/index.ts rename to apps/sn/app/(with-base-layout)/oper/index.ts diff --git a/apps/sn/app/oper/oldPp/page.tsx b/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx similarity index 96% rename from apps/sn/app/oper/oldPp/page.tsx rename to apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx index 92d7fe6aa..695ae7a28 100644 --- a/apps/sn/app/oper/oldPp/page.tsx +++ b/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx @@ -2,13 +2,12 @@ import { SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, UiTable } from '@common/business_components/ui' import { Button, DatePicker, Input, Label } from '@common/components/ui' import React, { useEffect, useState } from 'react' -import OperFileUploadForm from '../../components/ui/operFileUploadForm' +import OperFileUploadForm from '../../../components/ui/operFileUploadForm' import ListDiv from '@common/business_components/ui/list_div' import ContentLayout from '@/components/ui/contentLayout' import { tableHeadlistOldPp } from '..' -import { handleDownload } from '@/components/lib/utils/fileDownload' import { searchData } from '@/components/lib/common/apiHelpers' -import { formatDate, yearOptions } from '@/components/lib/utils/date' +import { formatDate, yearOptions, handleDownload } from '@/components/lib/utils' import { deleteData, postData } from '@/components/lib/api' const Page = () => { const [fromDate, setFromDate] = useState(()=>{const date = new Date(); date.setDate(date.getDate()-7); return date}); @@ -87,7 +86,7 @@ const Page = () => { if (data) {alert('파일이 등록되었습니다.');fetchData() } } return ( - + <>
@@ -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 { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, + ); + return response.data; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/sn/src/entities/menu/index.ts b/apps/sn/src/entities/menu/index.ts new file mode 100644 index 000000000..04f63e38b --- /dev/null +++ b/apps/sn/src/entities/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/sn/src/entities/menu/lib/menu-wrapper.tsx b/apps/sn/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..e00d3696e --- /dev/null +++ b/apps/sn/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu"; +import { useAlertStore, useMenuStore } from "@/shared/lib"; +import { getCookie } from "@/components/lib/utils"; +import { SITE_ID } from "@/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 }; diff --git a/apps/sn/src/entities/menu/lib/utils.ts b/apps/sn/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..1cf4514d9 --- /dev/null +++ b/apps/sn/src/entities/menu/lib/utils.ts @@ -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; +}; diff --git a/apps/sn/src/entities/menu/model/menu-interface.ts b/apps/sn/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..12c7d007a --- /dev/null +++ b/apps/sn/src/entities/menu/model/menu-interface.ts @@ -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; +} diff --git a/apps/sn/src/shared/api/client.ts b/apps/sn/src/shared/api/client.ts new file mode 100644 index 000000000..709bb44a5 --- /dev/null +++ b/apps/sn/src/shared/api/client.ts @@ -0,0 +1,8 @@ +import { Client } from "@common/network"; +import { BASE_URL } from "../config/api"; +import { SITE_ID } from "../config/env"; + +export const client = Client.getInstance({ + baseURL: BASE_URL, + siteId: SITE_ID, +}); diff --git a/apps/sn/src/shared/api/index.ts b/apps/sn/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/sn/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/sn/src/shared/config/api.ts b/apps/sn/src/shared/config/api.ts new file mode 100644 index 000000000..3eba9fa91 --- /dev/null +++ b/apps/sn/src/shared/config/api.ts @@ -0,0 +1,56 @@ +import { PROXY_HOST } from "./env"; + +export const BASE_URL = `${PROXY_HOST}/server`; + +export const ENDPOINT = { + ATTACH_SERVICE: { + TUS_UPLOAD: "/attach-service/api/v1/tus/upload", + EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", + IMAGES: "/attach-service/api/v1/images", + MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail", + ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + DOWNLOAD: "/attach-service/api/v1/atflcd/download", + STREAM_VIDEO: "/attach-service/api/v1/stream/video", + }, + 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", + ROLES: "/user-service/api/v1/roles", + }, + CMS_SERVICE: { + SITES: "/cms-service/api/v1/sites", + IPS: "/cms-service/api/v1/site-ips", + DOMAINS: "/cms-service/api/v1/site-dmns", + POSTINGS: "/cms-service/api/v1/site-pstgs", + CODE: "/cms-service/api/v1/code/cache", + MENUS: "/cms-service/api/v1/menus", + MENU_ROLES: "/cms-service/api/v1/menu-roles", + MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", + CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", + USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", + IMAGES: "/cms-service/api/v1/images", + BANNERS: "/cms-service/api/v1/bnrs", + TEMPLATE: "/cms-service/api/v1/tmplt", + TERMS: "/cms-service/api/v1/trms", + LWPO_CODE: "/cms-service/api/v1/lwpo-cds", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", + SCHEDULES: "/cms-service/api/v1/schdls", + SURVEY: "/cms-service/api/v1/srvy", + SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem", + SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl", + }, + BOARD_SERVICE: { + BBS: "/board-service/api/v1/bbs", + BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", + POSTS: "/board-service/api/v1/posts", + POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", + }, +}; + +export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/sn/src/shared/config/env.ts b/apps/sn/src/shared/config/env.ts new file mode 100644 index 000000000..c6373724f --- /dev/null +++ b/apps/sn/src/shared/config/env.ts @@ -0,0 +1,11 @@ +export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; +export const PROXY_HOST = + process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; +export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; +export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS"; +export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +export const ACCESS_TOKEN = + process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; +export const REFRESH_TOKEN = + process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; +export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; diff --git a/apps/sn/src/shared/config/index.ts b/apps/sn/src/shared/config/index.ts new file mode 100644 index 000000000..cd83447d5 --- /dev/null +++ b/apps/sn/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./env"; diff --git a/apps/sn/src/shared/lib/index.ts b/apps/sn/src/shared/lib/index.ts new file mode 100644 index 000000000..bca6d2476 --- /dev/null +++ b/apps/sn/src/shared/lib/index.ts @@ -0,0 +1,5 @@ +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"; diff --git a/apps/sn/src/shared/lib/use-alert-store.ts b/apps/sn/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/sn/src/shared/lib/use-alert-store.ts @@ -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 = (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); +}; diff --git a/apps/sn/src/shared/lib/use-menu-store.ts b/apps/sn/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/sn/src/shared/lib/use-menu-store.ts @@ -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 = (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); +}; diff --git a/apps/sn/src/shared/lib/use-mutation-wrapper.tsx b/apps/sn/src/shared/lib/use-mutation-wrapper.tsx new file mode 100644 index 000000000..ce119d2a7 --- /dev/null +++ b/apps/sn/src/shared/lib/use-mutation-wrapper.tsx @@ -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, +) => { + 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}`); + }, + }); +}; diff --git a/apps/sn/src/shared/lib/use-prompt.tsx b/apps/sn/src/shared/lib/use-prompt.tsx new file mode 100644 index 000000000..f36858eb0 --- /dev/null +++ b/apps/sn/src/shared/lib/use-prompt.tsx @@ -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 }; +}; diff --git a/apps/sn/src/shared/lib/use-user-store.ts b/apps/sn/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..748caed4c --- /dev/null +++ b/apps/sn/src/shared/lib/use-user-store.ts @@ -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 = (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); +}; diff --git a/apps/sn/src/shared/providers/index.ts b/apps/sn/src/shared/providers/index.ts new file mode 100644 index 000000000..fa8320fe7 --- /dev/null +++ b/apps/sn/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/sn/src/shared/providers/prompt-provider.tsx b/apps/sn/src/shared/providers/prompt-provider.tsx new file mode 100644 index 000000000..10338c338 --- /dev/null +++ b/apps/sn/src/shared/providers/prompt-provider.tsx @@ -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({ + message: "", + resolve() {}, + setMessage() {}, + setResolve() {}, +}); + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [message, setMessage] = useState(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); + + return ( + + {children} + + ); +}; diff --git a/apps/sn/app/store/store-provider.tsx b/apps/sn/src/shared/providers/store-provider.tsx similarity index 60% rename from apps/sn/app/store/store-provider.tsx rename to apps/sn/src/shared/providers/store-provider.tsx index 8a2e8347e..a0ec17b61 100644 --- a/apps/sn/app/store/store-provider.tsx +++ b/apps/sn/src/shared/providers/store-provider.tsx @@ -1,9 +1,17 @@ "use client"; import { createContext, useRef } from "react"; -import { createUserStore } from "../store/user-store"; +import { + createAlertStore, + createAttachmentStore, + createMenuStore, + createUserStore, +} from "../store"; type StoreContextType = { + alertStore: ReturnType; + attachmentStore: ReturnType; + menuStore: ReturnType; userStore: ReturnType; }; @@ -15,6 +23,9 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => { const storeRef = useRef(null); if (storeRef.current === null) { storeRef.current = { + alertStore: createAlertStore(), + attachmentStore: createAttachmentStore(), + menuStore: createMenuStore(), userStore: createUserStore(), }; } diff --git a/apps/sn/src/shared/store/alert-store.ts b/apps/sn/src/shared/store/alert-store.ts new file mode 100644 index 000000000..3bba65848 --- /dev/null +++ b/apps/sn/src/shared/store/alert-store.ts @@ -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()((set) => ({ + ...initState, + setMessage: (message: string, callback?: () => void) => + set(() => ({ message, callback })), + })); +}; diff --git a/apps/sn/src/shared/store/attachment-store.ts b/apps/sn/src/shared/store/attachment-store.ts new file mode 100644 index 000000000..faa700949 --- /dev/null +++ b/apps/sn/src/shared/store/attachment-store.ts @@ -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()((set) => ({ + ...initState, + setAttachmentFileCode: (attachmentFileCode: string) => + set({ attachmentFileCode }), + })); +}; diff --git a/apps/sn/src/shared/store/index.ts b/apps/sn/src/shared/store/index.ts new file mode 100644 index 000000000..a41d6f928 --- /dev/null +++ b/apps/sn/src/shared/store/index.ts @@ -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"; diff --git a/apps/sn/src/shared/store/menu-store.ts b/apps/sn/src/shared/store/menu-store.ts new file mode 100644 index 000000000..8536011ee --- /dev/null +++ b/apps/sn/src/shared/store/menu-store.ts @@ -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()((set) => ({ + ...initState, + setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }), + setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }), + setPageMenuAuthority: (authority: IAuthority) => + set({ pageMenuAuthority: authority }), + })); +}; diff --git a/apps/sn/src/shared/store/user-store.ts b/apps/sn/src/shared/store/user-store.ts new file mode 100644 index 000000000..65d897cf1 --- /dev/null +++ b/apps/sn/src/shared/store/user-store.ts @@ -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()((set) => ({ + ...initState, + setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), + })); +}; diff --git a/apps/sn/src/shared/ui/alert-dialog.tsx b/apps/sn/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..6160d5301 --- /dev/null +++ b/apps/sn/src/shared/ui/alert-dialog.tsx @@ -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 ( + + +
+

+ {message} +

+
+ + + + + +
+
+ ); +} + +export { AlertDialog }; diff --git a/apps/sn/src/shared/ui/confirm-dialog.tsx b/apps/sn/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..6a4996bfb --- /dev/null +++ b/apps/sn/src/shared/ui/confirm-dialog.tsx @@ -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 ( + + +
+

+ {message} +

+
+ + + + + + + + +
+
+ ); +} + +export { ConfirmDialog }; diff --git a/apps/sn/src/shared/ui/map-layout.tsx b/apps/sn/src/shared/ui/map-layout.tsx new file mode 100644 index 000000000..e05666401 --- /dev/null +++ b/apps/sn/src/shared/ui/map-layout.tsx @@ -0,0 +1,13 @@ +import { useMenuStore } from "../lib/use-menu-store"; + +function MapLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + return ( +
+ {children} +
+ ); +} + +export { MapLayout }; diff --git a/apps/sn/src/shared/ui/page-layout.tsx b/apps/sn/src/shared/ui/page-layout.tsx new file mode 100644 index 000000000..358d3b5db --- /dev/null +++ b/apps/sn/src/shared/ui/page-layout.tsx @@ -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 ( +
+ + {children} +
+ ); +} + +export { PageLayout }; diff --git a/apps/sn/src/shared/ui/prompt-dialog.tsx b/apps/sn/src/shared/ui/prompt-dialog.tsx new file mode 100644 index 000000000..54c2f3fcc --- /dev/null +++ b/apps/sn/src/shared/ui/prompt-dialog.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Input, +} from "@common/components"; +import { usePrompt } from "@/shared/lib/use-prompt"; + +function PromptDialog() { + const { message, isOpen, onSubmit, onCancel } = usePrompt(); + const [value, setValue] = useState(""); + + return ( + + +
+

+ {message} +

+ setValue(e.target.value)} + /> +
+ + + + + + + + +
+
+ ); +} + +export { PromptDialog }; diff --git a/apps/sn/tsconfig.json b/apps/sn/tsconfig.json index a65fab364..078e6ae2b 100644 --- a/apps/sn/tsconfig.json +++ b/apps/sn/tsconfig.json @@ -22,19 +22,17 @@ "name": "next" } ], - "baseUrl": "app", + "baseUrl": "./src", "paths": { - "@/*": ["./*"], - "@common/components/*": ["../../../packages/common/components/src/*"], + "@/*": ["*"], "@common/components": ["../../../packages/common/components/src"], - "@common/business_components/*": ["../../../packages/common/business_components/src/*"], + "@common/components/*": ["../../../packages/common/components/src/*"], "@common/business_components": ["../../../packages/common/business_components/src"], - "@common/network/*": ["../../../packages/common/network/*"], + "@common/business_components/*": ["../../../packages/common/business_components/src/*"], "@common/network": ["../../../packages/common/network/"], - "@store": ["./store"], - "@service": ["./service"], - "@utils": ["./components/lib/utils"], - "@common/config": ["../../../packages/common/config/dist/"] + "@common/config": ["../../../packages/common/config/dist/"], + "@utils": ["../app/components/lib/utils"], + "@/components/*": ["../app/components/*"] } }, "include": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c9b54a0c..54d7d12ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -317,6 +317,9 @@ importers: '@hookform/resolvers': specifier: ^4.1.3 version: 4.1.3(react-hook-form@7.54.2) + '@minoru/react-dnd-treeview': + specifier: 3.5.1-alpha.1 + version: 3.5.1-alpha.1(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) @@ -326,6 +329,9 @@ importers: '@tanstack/react-query': specifier: ^5.67.1 version: 5.67.1(react@18.3.1) + '@tanstack/react-query-devtools': + specifier: ^5.75.5 + version: 5.75.5(@tanstack/react-query@5.67.1)(react@18.3.1) '@tanstack/react-table': specifier: ^8.21.2 version: 8.21.2(react-dom@18.3.1)(react@18.3.1) @@ -356,6 +362,9 @@ importers: react-chartjs-2: specifier: ^5.3.0 version: 5.3.0(chart.js@4.4.8)(react@18.3.1) + react-dnd: + specifier: ^16.0.1 + version: 16.0.1(@types/node@22.13.9)(@types/react@18.3.18)(react@18.3.1) react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@18.3.1)