diff --git a/apps/as/docs/approval-line-management/SPEC.md b/apps/as/docs/approval-line-management/SPEC.md index c8994ab8a..8d76274ff 100644 --- a/apps/as/docs/approval-line-management/SPEC.md +++ b/apps/as/docs/approval-line-management/SPEC.md @@ -40,7 +40,7 @@ │ │ ⋮⋮ | APLN-0001 | [ 1 ] | [검토자 ▼] | [김민수 ][선택] | … | [저장][삭제] │ │ │ │ ⋮⋮ | (신규) | [ 2 ] | [승인자 ▼] | [ ][선택] | — | [저장][취소] │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────┘ │ -│ [행 추가] │ +│ [행 추가] │ │ ※ 결재자 등록·수정은 그리드 셀에서 직접 입력. 검토인력만 [선택] 팝업/다이얼로그 │ │ ※ 행 추가·수정 중: 해당 행만 편집 가능, 나머지 행·[행 추가]·드래그 비활성 │ └───────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/apps/as/package.json b/apps/as/package.json index a6954bdef..38bbc9fdd 100644 --- a/apps/as/package.json +++ b/apps/as/package.json @@ -24,6 +24,7 @@ "axios": "^1.8.1", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", + "lucide-react": "^0.474.0", "next": "15.3.8", "react-hook-form": "^7.54.2", "zod": "^3.24.2" diff --git a/apps/as/src/app/layouts/base-layout.tsx b/apps/as/src/app/layouts/base-layout.tsx index 54fb55c52..fc84a5e3e 100644 --- a/apps/as/src/app/layouts/base-layout.tsx +++ b/apps/as/src/app/layouts/base-layout.tsx @@ -5,6 +5,7 @@ import { usePathname, useRouter } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; +import { useMenuStore } from "@/shared/lib"; import { PageLayout } from "@/shared/ui/page-layout"; import type { MenuItem } from "@/entities/menu"; import { BaseLayout } from "@common/components"; @@ -18,11 +19,14 @@ export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) { const pathname = usePathname(); const queryClient = useQueryClient(); + const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); + const handleMenuClick = useCallback( (item: MenuItem) => { + setCurrentMenuInfo(item); router.push(item.urlPath); }, - [router], + [router, setCurrentMenuInfo], ); return ( diff --git a/apps/as/src/app/providers/index.tsx b/apps/as/src/app/providers/index.tsx index 476609d59..35ca749f8 100644 --- a/apps/as/src/app/providers/index.tsx +++ b/apps/as/src/app/providers/index.tsx @@ -5,7 +5,9 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { queryClient } from "@/shared/lib"; import { StoreProvider } from "@/shared/providers"; +import { AlertDialog } from "@/shared/ui/alert-dialog"; import { AuthWrapper } from "@/entities/auth"; +import { MenuWrapper } from "@/entities/menu"; interface ProvidersProps { children: React.ReactNode; @@ -15,7 +17,10 @@ export function Providers({ children }: ProvidersProps) { return ( - {children} + + {children} + + diff --git a/apps/as/src/entities/code/index.ts b/apps/as/src/entities/code/index.ts index ded8f7731..0f1e5e050 100644 --- a/apps/as/src/entities/code/index.ts +++ b/apps/as/src/entities/code/index.ts @@ -1,6 +1,5 @@ export { codeService } from "./api/code-service"; export { useCodeList, useCodeListByUseYn, useCodeName } from "./api/queries"; export { findCodeLabel } from "./lib/find-code-label"; -export { UP_CD } from "./lib/up-cd-constants"; -export { CodeSelect } from "./ui/code-select"; export type { Code, CodeService } from "./model/code-interface"; +export { CodeSelect } from "./ui/code-select"; diff --git a/apps/as/src/entities/code/lib/up-cd-constants.ts b/apps/as/src/entities/code/lib/up-cd-constants.ts deleted file mode 100644 index c85f4d986..000000000 --- a/apps/as/src/entities/code/lib/up-cd-constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** 결재선 관리 SPEC 상위코드 */ -export const UP_CD = { - DOC_SE: "ASDOCSECD", - ATRZ_SE: "ATRZSECD", -} as const; diff --git a/apps/as/src/entities/menu-authority/@x/menu.ts b/apps/as/src/entities/menu-authority/@x/menu.ts new file mode 100644 index 000000000..df6d6b3c0 --- /dev/null +++ b/apps/as/src/entities/menu-authority/@x/menu.ts @@ -0,0 +1 @@ +export { MenuAuthorityApi } from "../api/menu-authority-service"; diff --git a/apps/as/src/entities/menu-authority/api/menu-authority-service.ts b/apps/as/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..0aab142fd --- /dev/null +++ b/apps/as/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,19 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + MenuAuthorityService, + PageMenuAuthority, +} from "../model/menu-authority-interface"; + +export class MenuAuthorityApiService implements MenuAuthorityService { + /** MenuAuthrtApiController GET /api/v1/menu-authrts/pageinfo/{menuCd} */ + async getPageMenuAuthority(menuCd: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`, + ); + return response.data; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/as/src/entities/menu-authority/index.ts b/apps/as/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..ce3b8781f --- /dev/null +++ b/apps/as/src/entities/menu-authority/index.ts @@ -0,0 +1,2 @@ +export { MenuAuthorityApi } from "./api/menu-authority-service"; +export type { PageMenuAuthority } from "./model/menu-authority-interface"; diff --git a/apps/as/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/as/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..f61abbe66 --- /dev/null +++ b/apps/as/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,12 @@ +/** MenuAuthrtApiController GET /pageinfo/{menuCd} 응답 DTO */ +export interface PageMenuAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export interface MenuAuthorityService { + getPageMenuAuthority(menuCd: number): Promise; +} diff --git a/apps/as/src/entities/menu/api/menu-service.ts b/apps/as/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..40c6c9327 --- /dev/null +++ b/apps/as/src/entities/menu/api/menu-service.ts @@ -0,0 +1,18 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import { mapMenuTreeResponse } from "../lib/map-menu-tree-response"; +import type { MenuTreeResponseDto } from "../model/menu-tree-response"; +import type { MenuItem } from "../model/types"; + +export class MenuApiService { + /** MenuRoleApiController GET /api/v1/menu-roles/{siteId} */ + async getMenuRoles(siteId: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, + ); + return response.data.map(mapMenuTreeResponse); + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/as/src/entities/menu/index.ts b/apps/as/src/entities/menu/index.ts index cb55d9a1c..eb1813456 100644 --- a/apps/as/src/entities/menu/index.ts +++ b/apps/as/src/entities/menu/index.ts @@ -1 +1,4 @@ +export { MenuApi } from "./api/menu-service"; +export { MenuWrapper } from "./lib/menu-wrapper"; +export { getCurrentMenu } from "./lib/utils"; export { type MenuItem } from "./model/types"; diff --git a/apps/as/src/entities/menu/lib/map-menu-tree-response.ts b/apps/as/src/entities/menu/lib/map-menu-tree-response.ts new file mode 100644 index 000000000..edbf57eb6 --- /dev/null +++ b/apps/as/src/entities/menu/lib/map-menu-tree-response.ts @@ -0,0 +1,28 @@ +import type { MenuTreeResponseDto } from "../model/menu-tree-response"; +import type { MenuItem } from "../model/types"; + +function isRequiredUrlPath(menuLnkgTypeCd: string): boolean { + return menuLnkgTypeCd === "BOARD" || menuLnkgTypeCd === "CONTENTS"; +} + +export function mapMenuTreeResponse(dto: MenuTreeResponseDto): MenuItem { + return { + menuCd: dto.menuCd, + menuKornNm: dto.menuKornNm, + menuEngNm: dto.menuEngNm, + parentId: dto.parentId, + sortSeq: dto.sortSeq, + iconNm: dto.iconNm, + children: dto.children?.map(mapMenuTreeResponse) ?? null, + levelNo: dto.levelNo, + urlPath: dto.urlPath, + menuLnkgTypeCd: dto.menuLnkgTypeCd, + menuLnkgSn: dto.menuLnkgSn, + menuPath: dto.menuPath, + qrcd: dto.qrcd, + useYn: dto.useYn, + otptYn: dto.otptYn, + newWndwYn: dto.newWndwYn, + requiredUrlPath: isRequiredUrlPath(dto.menuLnkgTypeCd), + }; +} diff --git a/apps/as/src/entities/menu/lib/menu-wrapper.tsx b/apps/as/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..c1a4d2b1f --- /dev/null +++ b/apps/as/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; + +import { SITE_ID } from "@/shared/config"; +import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; +import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu"; + +import { MenuApi } from "../api/menu-service"; + +import { getCurrentMenu } from "./utils"; + +function 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, 10), + ); + 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]!); + })(); + }, [pathname]); + + useEffect(() => { + if (!pageMenuAuthority) return; + const inquiryAuthority = checkInquiryAuthority(); + if (!inquiryAuthority) { + alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); + } + }, [pageMenuAuthority]); + + return <>{children}; +} + +export { MenuWrapper }; diff --git a/apps/as/src/entities/menu/lib/utils.ts b/apps/as/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..d62ccbd93 --- /dev/null +++ b/apps/as/src/entities/menu/lib/utils.ts @@ -0,0 +1,21 @@ +import type { MenuItem } from "../model/types"; + +export function 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 child = getCurrentMenu(menu.children, pathname); + if (child) return child; + } + } + return null; +} diff --git a/apps/as/src/entities/menu/model/menu-tree-response.ts b/apps/as/src/entities/menu/model/menu-tree-response.ts new file mode 100644 index 000000000..dc57c90da --- /dev/null +++ b/apps/as/src/entities/menu/model/menu-tree-response.ts @@ -0,0 +1,21 @@ +/** MenuRoleApiController GET /api/v1/menu-roles/{siteId} 응답 DTO */ +export interface MenuTreeResponseDto { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuTreeResponseDto[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + menuHlpCn?: string; + menuSubKornNm?: string; +} diff --git a/apps/as/src/features/elapr-tmplt-dialog/ui/elapr-tmplt-form-dialog.tsx b/apps/as/src/features/elapr-tmplt-dialog/ui/elapr-tmplt-form-dialog.tsx index e4c0b94fe..0836fc3b6 100644 --- a/apps/as/src/features/elapr-tmplt-dialog/ui/elapr-tmplt-form-dialog.tsx +++ b/apps/as/src/features/elapr-tmplt-dialog/ui/elapr-tmplt-form-dialog.tsx @@ -2,6 +2,10 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; + +import { generateClientId } from "@/shared/lib"; +import { CodeSelect } from "@/entities/code"; +import type { ElaprTmplt } from "@/entities/elapr"; import { Button, Dialog, @@ -14,10 +18,6 @@ import { Textarea, } from "@common/components"; -import { CodeSelect, UP_CD } from "@/entities/code"; -import type { ElaprTmplt } from "@/entities/elapr"; -import { generateClientId } from "@/shared/lib"; - export interface ElaprTmpltFormValues { elaprTmpltId: string; elaprTmpltNm: string; @@ -77,41 +77,40 @@ function ElaprTmpltFormDialog({ return ( - + {mode === "create" ? "결재서식 등록" : "결재서식 수정"}
- +
- +
setValue("docSeComCd", v)} placeholder="문서구분 선택" + className="w-full" />
-