소스 구조 수정 및 메뉴 적용
This commit is contained in:
@@ -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 (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
{children}
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -202,7 +202,6 @@ const BaseLayout = ({
|
||||
|
||||
return (
|
||||
<Form {...form} className="min-h-screen flex flex-col">
|
||||
<ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden" isLoading={isLoading}>
|
||||
<ListDiv>
|
||||
{/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */}
|
||||
{selectBox?.title && (
|
||||
@@ -268,7 +267,6 @@ const BaseLayout = ({
|
||||
<div className="card !m-[1.2rem]">{children?.[1]}</div>
|
||||
</ListDiv> : null
|
||||
}
|
||||
</ContentLayout >
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -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({
|
||||
@@ -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 스키마 정의
|
||||
+1
-1
@@ -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({
|
||||
@@ -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({
|
||||
@@ -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 (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<PageLayout>{children}</PageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
+2
-3
@@ -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'
|
||||
|
||||
+4
-5
@@ -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<Date>(()=>{const date = new Date(); date.setDate(date.getDate()-7); return date});
|
||||
@@ -87,7 +86,7 @@ const Page = () => {
|
||||
if (data) {alert('파일이 등록되었습니다.');fetchData() }
|
||||
}
|
||||
return (
|
||||
<ContentLayout title='노후 하수관로 정비사업' isLoading={isLoading}>
|
||||
<>
|
||||
<SearchDiv>
|
||||
<SearchFormLeft>
|
||||
<div className='flex items-center gap-4'>
|
||||
@@ -154,7 +153,7 @@ const Page = () => {
|
||||
<ListDiv className="mt-4">
|
||||
<OperFileUploadForm title='노후 하수관로 정비사업 주진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} />
|
||||
</ListDiv>
|
||||
</ContentLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
|
||||
export function ClientLayout({ children }: { children: React.ReactNode }) {
|
||||
return <BaseLayout>{children}</BaseLayout>;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
|
||||
+2
-25
@@ -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 (
|
||||
<html lang="en" data-theme={theme?.value}>
|
||||
<body>
|
||||
<StoreProvider>
|
||||
<AuthWrapper>
|
||||
<ClientLayout>{children}</ClientLayout>
|
||||
</AuthWrapper>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
export default App;
|
||||
+33
-14
@@ -1,27 +1,46 @@
|
||||
'use client'
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@common/components/ui";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<div><h3 className="text-2xl text-bold gap-4 mx-4">하수관망도</h3>
|
||||
<div className="flex flex-row gap-4 p-4">
|
||||
<Link href={"/map"}><Button>하수관망도</Button></Link>
|
||||
</div>
|
||||
</div>
|
||||
<div><h3 className="text-2xl text-bold gap-4 mx-4">운영정보 관리</h3>
|
||||
<div>
|
||||
<h3 className="text-bold mx-4 gap-4 text-2xl">하수관망도</h3>
|
||||
<div className="flex flex-row gap-4 p-4">
|
||||
<Link href={"/oper/floodPre"}><Button>도시침수 예방사업</Button></Link>
|
||||
<Link href={"/oper/oldPp"}><Button>노후 하수관로 정비사업</Button></Link>
|
||||
<Link href={"/map"}>
|
||||
<Button>하수관망도</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div><h3 className="text-2xl text-bold gap-4 mx-4">I/I 분석(기초정보 설정)</h3>
|
||||
<div>
|
||||
<h3 className="text-bold mx-4 gap-4 text-2xl">운영정보 관리</h3>
|
||||
<div className="flex flex-row gap-4 p-4">
|
||||
<Link href={"/ii/irsetting"}><Button>침입수 분석</Button></Link>
|
||||
<Link href={"/ii/rdii"}><Button>RDII 분석</Button></Link>
|
||||
<Link href={"/ii/result"}><Button>침입수 분석 결과</Button></Link>
|
||||
<Link href={"/ii/rdii_result"}><Button>RDII 분석 결과</Button></Link>
|
||||
<Link href={"/oper/floodPre"}>
|
||||
<Button>도시침수 예방사업</Button>
|
||||
</Link>
|
||||
<Link href={"/oper/oldPp"}>
|
||||
<Button>노후 하수관로 정비사업</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-bold mx-4 gap-4 text-2xl">
|
||||
I/I 분석(기초정보 설정)
|
||||
</h3>
|
||||
<div className="flex flex-row gap-4 p-4">
|
||||
<Link href={"/ii/irsetting"}>
|
||||
<Button>침입수 분석</Button>
|
||||
</Link>
|
||||
<Link href={"/ii/rdii"}>
|
||||
<Button>RDII 분석</Button>
|
||||
</Link>
|
||||
<Link href={"/ii/result"}>
|
||||
<Button>침입수 분석 결과</Button>
|
||||
</Link>
|
||||
<Link href={"/ii/rdii_result"}>
|
||||
<Button>RDII 분석 결과</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './map'
|
||||
@@ -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`)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { useUserStore, type IUserInfo } from "./user-store";
|
||||
@@ -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<UserStore>()(
|
||||
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<UserStore>()((set) => ({
|
||||
...initState,
|
||||
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
|
||||
}));
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
return (
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<body>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export { App };
|
||||
@@ -0,0 +1,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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider
|
||||
backend={MultiBackend}
|
||||
options={getBackendOptions()}
|
||||
>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
<div className="text-2xl">
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { AppProvider };
|
||||
@@ -0,0 +1 @@
|
||||
export { AppProvider } from "./app-provider";
|
||||
+12
-32
@@ -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<void>;
|
||||
silentRefresh(): Promise<void>;
|
||||
}
|
||||
|
||||
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");
|
||||
+9
-19
@@ -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}</>;
|
||||
};
|
||||
+2
-4
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { MenuAuthorityApi } from "../api/menu-authority-service";
|
||||
@@ -0,0 +1 @@
|
||||
export { AuthorityButton } from "../ui/authority-button";
|
||||
@@ -0,0 +1,40 @@
|
||||
import { client } from "@/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<MenuAuthorityList> {
|
||||
const response = await client.get<MenuAuthorityList>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPageMenuAuthority(
|
||||
menuCd: number,
|
||||
): Promise<PageMenuAuthority[]> {
|
||||
const response = await client.get<PageMenuAuthority[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuAuthorityApi = new MenuAuthorityApiService();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { AuthorityButton } from "./ui/authority-button";
|
||||
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface MenuAuthorityParams extends PageableParams {
|
||||
groupId?: string;
|
||||
menuCd?: number;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface PageMenuAuthority {
|
||||
mngAuthrtYn: string;
|
||||
inqAuthrtYn: string;
|
||||
regAuthrtYn: string;
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
}
|
||||
|
||||
export const menuAuthoritySchema = z.object({
|
||||
groupId: z.string(),
|
||||
groupNm: z.string(),
|
||||
menuCd: z.number(),
|
||||
mngAuthrtYn: z.string(),
|
||||
inqAuthrtYn: z.string(),
|
||||
regAuthrtYn: z.string(),
|
||||
mdfcnAuthrtYn: z.string(),
|
||||
delAuthrtYn: z.string(),
|
||||
useYn: z.string(),
|
||||
mode: z.string(),
|
||||
});
|
||||
|
||||
export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({
|
||||
groupNm: true,
|
||||
});
|
||||
|
||||
export const menuAuthorityFormSchema = z.object({
|
||||
menuAuthority: z.array(menuAuthorityPayloadSchema),
|
||||
});
|
||||
|
||||
export type MenuAuthority = z.infer<typeof menuAuthoritySchema>;
|
||||
export type MenuAuthorityList = PageableList<MenuAuthority>;
|
||||
export type MenuAuthorityPayload = z.infer<typeof menuAuthorityPayloadSchema>;
|
||||
export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
|
||||
|
||||
export interface MenuAuthorityService {
|
||||
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
|
||||
getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>;
|
||||
saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type {
|
||||
MenuAuthority,
|
||||
MenuAuthorityFormData,
|
||||
MenuAuthorityPayload,
|
||||
} from "./menu-authority-interface";
|
||||
|
||||
export const createColumns = (
|
||||
form: UseFormReturn<MenuAuthorityFormData>,
|
||||
): ColumnDef<MenuAuthority>[] => {
|
||||
const getCurrentRow = (i: number) => form.watch("menuAuthority")[i];
|
||||
|
||||
const handleCheckedChange =
|
||||
(i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => {
|
||||
if (getCurrentRow(i)?.mode === "R") {
|
||||
form.setValue(`menuAuthority.${i}.mode`, "U");
|
||||
}
|
||||
form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N");
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: "groupNm",
|
||||
header: "그룹명",
|
||||
},
|
||||
{
|
||||
accessorKey: "mngAuthrtYn",
|
||||
header: "관리권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mngAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"regAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"mdfcnAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"delAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "regAuthrtYn",
|
||||
header: "등록권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.regAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnAuthrtYn",
|
||||
header: "수정권한 ",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mdfcnAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "mdfcnAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "delAuthrtYn",
|
||||
header: "삭제권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.delAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "delAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "inqAuthrtYn",
|
||||
header: "조회권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.inqAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "inqAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { Button, cn, type ButtonProps } from "@common/components";
|
||||
import type { PageMenuAuthority } from "../model/menu-authority-interface";
|
||||
|
||||
type AuthorityButtonProps = {
|
||||
authority: keyof PageMenuAuthority;
|
||||
} & ButtonProps;
|
||||
|
||||
function AuthorityButton({
|
||||
authority,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AuthorityButtonProps) {
|
||||
const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority);
|
||||
|
||||
const isAuthority = pageMenuAuthority?.[authority] === "Y";
|
||||
|
||||
return (
|
||||
<Button className={cn(className, !isAuthority && "hidden")} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { AuthorityButton };
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import {
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthorityFormData,
|
||||
type MenuAuthorityPayload,
|
||||
} from "../model/menu-authority-interface";
|
||||
import { createColumns } from "../model/menu-authority-table-columns";
|
||||
import { MenuAuthorityApi } from "../api/menu-authority-service";
|
||||
|
||||
type ManagementAuthorityDialogProps = {
|
||||
menuCd?: number;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function ManagementAuthorityDialog({
|
||||
menuCd,
|
||||
...props
|
||||
}: ManagementAuthorityDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: menuAuthorityList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd],
|
||||
queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }),
|
||||
enabled: !!menuCd,
|
||||
});
|
||||
|
||||
const { mutateAsync: saveMenuAuthority } = useMutationWrapper({
|
||||
mutationFn: (data: MenuAuthorityPayload[]) =>
|
||||
MenuAuthorityApi.saveMenuAuthority(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuAuthorityFormData>({
|
||||
resolver: zodResolver(menuAuthorityFormSchema),
|
||||
values: {
|
||||
menuAuthority: menuAuthorityList?.content ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: menuAuthorityList?.content ?? [],
|
||||
columns: createColumns(form),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const data = form.getValues("menuAuthority");
|
||||
const filteredData = data.filter((item) => item.mode !== "R");
|
||||
await saveMenuAuthority(filteredData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3" disabled={!menuCd}>
|
||||
<i className="diveicon di-personal-information" />
|
||||
권한관리
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[40rem] overflow-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ManagementAuthorityDialog };
|
||||
@@ -0,0 +1,14 @@
|
||||
import { client } from "@/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<MenuItem[]> {
|
||||
const response = await client.get<MenuItem[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
export type { MenuItem } from "./model/menu-interface";
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { MenuAuthorityApi } from "@/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 };
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { MenuItem } from "../model/menu-interface";
|
||||
|
||||
export const getCurrentMenu = (
|
||||
menus: MenuItem[],
|
||||
pathname: string,
|
||||
): MenuItem | null => {
|
||||
for (const menu of menus) {
|
||||
if (
|
||||
menu.urlPath !== "" &&
|
||||
(menu.urlPath === pathname || pathname.startsWith(menu.urlPath))
|
||||
)
|
||||
return menu;
|
||||
|
||||
if (menu.children) {
|
||||
const children = getCurrentMenu(menu.children, pathname);
|
||||
if (children) return children;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface MenuItem {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
export interface MenuService {
|
||||
getMenuRoles(siteId: string): Promise<MenuItem[]>;
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { client } from "./client";
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./api";
|
||||
export * from "./env";
|
||||
@@ -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";
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { AlertStore } from "../store/alert-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useAlertStore = <T>(selector: (store: AlertStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useAlertStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.alertStore, selector);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { MenuStore } from "../store/menu-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useMenuStore = <T>(selector: (store: MenuStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useMenuStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.menuStore, selector);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
|
||||
import { useAlertStore } from "./use-alert-store";
|
||||
|
||||
export const useMutationWrapper = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
TVariables = void,
|
||||
TContext = unknown,
|
||||
>(
|
||||
mutationOptions: UseMutationOptions<TData, TError, TVariables, TContext>,
|
||||
) => {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
return useMutation({
|
||||
...mutationOptions,
|
||||
onError: (error) => {
|
||||
let errorMessage = "";
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status)
|
||||
errorMessage += `status: ${error.response.status}\n`;
|
||||
if (error.response?.data?.message)
|
||||
errorMessage += `${error.response.data.message}\n`;
|
||||
if (error.response?.data?.errors[0]?.message)
|
||||
errorMessage += `${error.response.data.errors[0].message}\n`;
|
||||
}
|
||||
|
||||
alert(`[Error] ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useContext } from "react";
|
||||
import { PromptContext } from "../providers/prompt-provider";
|
||||
|
||||
export const usePrompt = () => {
|
||||
const { message, resolve, setMessage, setResolve } =
|
||||
useContext(PromptContext);
|
||||
|
||||
const prompt = (message: string) => {
|
||||
setMessage(message);
|
||||
return new Promise<{ resolved: boolean; value?: string }>((resolve) => {
|
||||
setResolve(() => resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (value: string) => {
|
||||
resolve?.({ resolved: true, value });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
resolve?.({ resolved: false });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
return { message, isOpen: !!message, prompt, onSubmit, onCancel };
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { UserStore } from "../store/user-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useUserStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.userStore, selector);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { StoreProvider } from "./store-provider";
|
||||
export { PromptProvider } from "./prompt-provider";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
interface PromptContext {
|
||||
message?: string;
|
||||
resolve?: ({
|
||||
resolved,
|
||||
value,
|
||||
}: {
|
||||
resolved: boolean;
|
||||
value?: string;
|
||||
}) => void;
|
||||
setMessage: (message: string) => void;
|
||||
setResolve: (resolve: () => void) => void;
|
||||
}
|
||||
|
||||
export const PromptContext = createContext<PromptContext>({
|
||||
message: "",
|
||||
resolve() {},
|
||||
setMessage() {},
|
||||
setResolve() {},
|
||||
});
|
||||
|
||||
export const PromptProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [message, setMessage] = useState<string>();
|
||||
const [resolve, setResolve] =
|
||||
useState<
|
||||
({ resolved, value }: { resolved: boolean; value?: string }) => void
|
||||
>();
|
||||
|
||||
return (
|
||||
<PromptContext.Provider
|
||||
value={{ message, resolve, setMessage, setResolve }}
|
||||
>
|
||||
{children}
|
||||
</PromptContext.Provider>
|
||||
);
|
||||
};
|
||||
+12
-1
@@ -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<typeof createAlertStore>;
|
||||
attachmentStore: ReturnType<typeof createAttachmentStore>;
|
||||
menuStore: ReturnType<typeof createMenuStore>;
|
||||
userStore: ReturnType<typeof createUserStore>;
|
||||
};
|
||||
|
||||
@@ -15,6 +23,9 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const storeRef = useRef<StoreContextType | null>(null);
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = {
|
||||
alertStore: createAlertStore(),
|
||||
attachmentStore: createAttachmentStore(),
|
||||
menuStore: createMenuStore(),
|
||||
userStore: createUserStore(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export type AlertState = {
|
||||
message: string;
|
||||
callback?: () => void;
|
||||
};
|
||||
|
||||
export type AlertActions = {
|
||||
setMessage: (message: string, callback?: () => void) => void;
|
||||
};
|
||||
|
||||
export type AlertStore = AlertState & AlertActions;
|
||||
|
||||
export const defaultInitState: AlertState = {
|
||||
message: "",
|
||||
callback: undefined,
|
||||
};
|
||||
|
||||
export const createAlertStore = (initState: AlertState = defaultInitState) => {
|
||||
return createStore<AlertStore>()((set) => ({
|
||||
...initState,
|
||||
setMessage: (message: string, callback?: () => void) =>
|
||||
set(() => ({ message, callback })),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export type AttachmentState = {
|
||||
attachmentFileCode: string;
|
||||
};
|
||||
|
||||
export type AttachmentActions = {
|
||||
setAttachmentFileCode: (attachmentFileCode: string) => void;
|
||||
};
|
||||
|
||||
export type AttachmentStore = AttachmentState & AttachmentActions;
|
||||
|
||||
export const defaultInitState: AttachmentState = {
|
||||
attachmentFileCode: "",
|
||||
};
|
||||
|
||||
export const createAttachmentStore = (
|
||||
initState: AttachmentState = defaultInitState,
|
||||
) => {
|
||||
return createStore<AttachmentStore>()((set) => ({
|
||||
...initState,
|
||||
setAttachmentFileCode: (attachmentFileCode: string) =>
|
||||
set({ attachmentFileCode }),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export { createAlertStore } from "./alert-store";
|
||||
export { createAttachmentStore } from "./attachment-store";
|
||||
export { createMenuStore, type BbsType } from "./menu-store";
|
||||
export { createUserStore, type IUserInfo } from "./user-store";
|
||||
@@ -0,0 +1,93 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export const BBS_TYPE = {
|
||||
BBSNOTICE: "BBSNOTICE",
|
||||
BBSNORMAL: "BBSNORMAL",
|
||||
BBSFAQ: "BBSFAQ",
|
||||
BBSIMAGE: "BBSIMAGE",
|
||||
BBSMOVIE: "BBSMOVIE",
|
||||
} as const;
|
||||
|
||||
export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE];
|
||||
|
||||
export interface IBbs {
|
||||
bbsId: number;
|
||||
bbsTypeCd: BbsType;
|
||||
bbsTypeNm: string;
|
||||
bbsNm: string;
|
||||
bbsExpln: string;
|
||||
rfrncUseYn: string;
|
||||
atflUseYn: string;
|
||||
atflMaxCnt: number;
|
||||
atflMaxFileSz: number;
|
||||
ansUseYn: string;
|
||||
ansUseAuthrt: string;
|
||||
cmntUseYn: string;
|
||||
dgstfnEvlUseYn: string;
|
||||
smsUseYn: string;
|
||||
upndFixUseYn: string;
|
||||
pstgPrdUseYn: string;
|
||||
popupUseYn: string;
|
||||
delYn: string;
|
||||
useYn: string;
|
||||
mdfcnDt: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
interface IMenu {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: IMenu[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
interface IAuthority {
|
||||
mngAuthrtYn: string;
|
||||
inqAuthrtYn: string;
|
||||
regAuthrtYn: string;
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
}
|
||||
|
||||
export type MenuState = {
|
||||
bbsInfo: IBbs | null;
|
||||
currentMenuInfo: IMenu | null;
|
||||
pageMenuAuthority: IAuthority | null;
|
||||
};
|
||||
|
||||
export type MenuActions = {
|
||||
setBbsInfo: (bbs: IBbs) => void;
|
||||
setCurrentMenuInfo: (menu: IMenu) => void;
|
||||
setPageMenuAuthority: (authority: IAuthority) => void;
|
||||
};
|
||||
|
||||
export type MenuStore = MenuState & MenuActions;
|
||||
|
||||
export const defaultInitState: MenuState = {
|
||||
currentMenuInfo: null,
|
||||
bbsInfo: null,
|
||||
pageMenuAuthority: null,
|
||||
};
|
||||
|
||||
export const createMenuStore = (initState: MenuState = defaultInitState) => {
|
||||
return createStore<MenuStore>()((set) => ({
|
||||
...initState,
|
||||
setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }),
|
||||
setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }),
|
||||
setPageMenuAuthority: (authority: IAuthority) =>
|
||||
set({ pageMenuAuthority: authority }),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export interface IUserInfo {
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgtr: string;
|
||||
mdfr: string;
|
||||
adminRoleYn: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
encryptedPassword: string;
|
||||
refreshToken: string;
|
||||
userStateCode: string;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
pswdHnt: string;
|
||||
pswdAns: string;
|
||||
mphn: string;
|
||||
telno: string;
|
||||
zip: string;
|
||||
addr: string;
|
||||
daddr: string;
|
||||
laeSeCd: string;
|
||||
ogdpSeCd: string;
|
||||
aprvYn: string;
|
||||
autzrId: string;
|
||||
expryYmd: string;
|
||||
lastPswdChgDt: string;
|
||||
googleId: string;
|
||||
kakaoId: string;
|
||||
naverId: string;
|
||||
isSocialUser: boolean;
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
export type UserState = {
|
||||
userInfo: IUserInfo | null;
|
||||
};
|
||||
|
||||
export type UserActions = {
|
||||
setUserInfo: (userInfo: IUserInfo | null) => void;
|
||||
};
|
||||
|
||||
export type UserStore = UserState & UserActions;
|
||||
|
||||
export const defaultInitState: UserState = {
|
||||
userInfo: null,
|
||||
};
|
||||
|
||||
export const createUserStore = (initState: UserState = defaultInitState) => {
|
||||
return createStore<UserStore>()((set) => ({
|
||||
...initState,
|
||||
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
} from "@common/components";
|
||||
import { useAlertStore } from "../lib/use-alert-store";
|
||||
|
||||
function AlertDialog() {
|
||||
const { message, callback, setMessage } = useAlertStore((state) => state);
|
||||
const handleOpenChange = () => {
|
||||
setMessage("");
|
||||
callback?.();
|
||||
};
|
||||
return (
|
||||
<Dialog open={!!message} onOpenChange={handleOpenChange}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button>확인</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AlertDialog };
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
type ButtonProps,
|
||||
} from "@common/components";
|
||||
|
||||
type ConfirmDialogProps = {
|
||||
isOpen?: boolean;
|
||||
confirmButtonProps?: ButtonProps & { label?: string };
|
||||
cancelButtonProps?: ButtonProps & { label?: string };
|
||||
};
|
||||
|
||||
function ConfirmDialog({
|
||||
isOpen,
|
||||
confirmButtonProps,
|
||||
cancelButtonProps,
|
||||
}: ConfirmDialogProps) {
|
||||
const { message, isOpen: isGlobalOpen, onConfirm, onCancel } = useConfirm();
|
||||
return (
|
||||
<Dialog open={isOpen ?? isGlobalOpen} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onConfirm} {...confirmButtonProps}>
|
||||
{confirmButtonProps?.label ?? "확인"}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onCancel} color="white" {...cancelButtonProps}>
|
||||
{cancelButtonProps?.label ?? "취소"}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ConfirmDialog };
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useMenuStore } from "../lib/use-menu-store";
|
||||
|
||||
function MapLayout({ children }: { children: React.ReactNode }) {
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MapLayout };
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PageTitle } from "@common/components";
|
||||
import { useMenuStore } from "../lib/use-menu-store";
|
||||
|
||||
function PageLayout({ children }: { children: React.ReactNode }) {
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
const menuPaths = currentMenuInfo?.menuPath
|
||||
?.split(">")
|
||||
.map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
<PageTitle
|
||||
title={currentMenuInfo?.menuKornNm ?? ""}
|
||||
menuPaths={menuPaths ?? []}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageLayout };
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { usePrompt } from "@/shared/lib/use-prompt";
|
||||
|
||||
function PromptDialog() {
|
||||
const { message, isOpen, onSubmit, onCancel } = usePrompt();
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="flex flex-col gap-6 pb-4 pt-8">
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
className="!text-[1.3rem]"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button onClick={() => onSubmit(value)}>확인</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onCancel} color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PromptDialog };
|
||||
@@ -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": [
|
||||
|
||||
Generated
+9
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user