From 29fbda149b97611429c3a20ca8e236f0a8e80163 Mon Sep 17 00:00:00 2001 From: sohot8653 Date: Tue, 23 Sep 2025 14:15:32 +0900 Subject: [PATCH] =?UTF-8?q?[=EB=A9=94=EB=89=B4=20=EA=B6=8C=ED=95=9C=201?= =?UTF-8?q?=EC=B0=A8=20=EC=9E=91=EC=97=85]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/layout/AppHeader.vue | 53 +-- composables/usePermission.ts | 43 +++ layouts/default.vue | 182 +++------ middleware/auth.ts | 17 +- pages/[tabId]/common-test.vue | 414 -------------------- pages/[tabId]/test/common-test.vue | 497 ++++++++++++++++++++++++ pages/admin/permission-test.vue | 589 +++++++++++++++++++++++++++++ stores/permissions.ts | 230 +++++++++++ stores/user.ts | 34 +- types/permissions.ts | 345 +++++++++++++++++ 10 files changed, 1808 insertions(+), 596 deletions(-) create mode 100644 composables/usePermission.ts delete mode 100644 pages/[tabId]/common-test.vue create mode 100644 pages/[tabId]/test/common-test.vue create mode 100644 pages/admin/permission-test.vue create mode 100644 stores/permissions.ts create mode 100644 types/permissions.ts diff --git a/components/layout/AppHeader.vue b/components/layout/AppHeader.vue index d942bc2..fe766ff 100644 --- a/components/layout/AppHeader.vue +++ b/components/layout/AppHeader.vue @@ -7,26 +7,20 @@ - + + - - @@ -77,22 +71,35 @@ - - diff --git a/pages/[tabId]/test/common-test.vue b/pages/[tabId]/test/common-test.vue new file mode 100644 index 0000000..662ec56 --- /dev/null +++ b/pages/[tabId]/test/common-test.vue @@ -0,0 +1,497 @@ + + + + + diff --git a/pages/admin/permission-test.vue b/pages/admin/permission-test.vue new file mode 100644 index 0000000..36115a1 --- /dev/null +++ b/pages/admin/permission-test.vue @@ -0,0 +1,589 @@ + + + diff --git a/stores/permissions.ts b/stores/permissions.ts new file mode 100644 index 0000000..da01b9f --- /dev/null +++ b/stores/permissions.ts @@ -0,0 +1,230 @@ +import type { UserPermissions, Resource } from "~/types/permissions"; +import { MOCK_PERMISSIONS } from "~/types/permissions"; + +export const usePermissionsStore = defineStore( + "permissions", + () => { + // 권한 데이터 상태 + const permissions = ref({ + resources: { + menus: [], + pages: [], + components: [], + }, + }); + + // 권한 로딩 상태 + const isLoading = ref(false); + + // 서버에서 권한 데이터 가져오기 (현재는 가데이터 사용) + const fetchPermissions = async (): Promise => { + try { + isLoading.value = true; + + // 실제 API 호출 (백엔드 준비되면 주석 해제) + /* + const { success, data } = await useApi<{ + pagePermissions: string[]; + menuPermissions: string[]; + componentPermissions: string[]; + }>('/auth/permissions', { + method: 'GET', + handleError: false + }); + + if (success && data) { + permissions.value = { + pagePermissions: data.pagePermissions || [], + menuPermissions: data.menuPermissions || [], + componentPermissions: data.componentPermissions || [] + }; + return true; + } + return false; + */ + + // 임시 가데이터 사용 + await new Promise(resolve => setTimeout(resolve, 500)); // 로딩 시뮬레이션 + + permissions.value = { + resources: { + menus: [...MOCK_PERMISSIONS.resources.menus], + pages: [...MOCK_PERMISSIONS.resources.pages], + components: [...MOCK_PERMISSIONS.resources.components], + }, + }; + + return true; + } catch (error) { + console.error("권한 데이터 로드 실패:", error); + return false; + } finally { + isLoading.value = false; + } + }; + + // 헬퍼 함수들 - 기존 호환성을 위한 함수들 + const getPagePaths = (): string[] => { + return permissions.value.resources.pages + .map(page => page.path) + .filter(Boolean) as string[]; + }; + + const getMenuCodes = (): string[] => { + return permissions.value.resources.menus.map(menu => menu.code); + }; + + const getComponentCodes = (): string[] => { + return permissions.value.resources.components.map( + component => component.code + ); + }; + + // 권한 체크 함수들 (기존 호환성 유지) + const hasPagePermission = (page: string): boolean => { + return getPagePaths().includes(page); + }; + + const hasMenuPermission = (menu: string): boolean => { + return getMenuCodes().includes(menu); + }; + + const hasComponentPermission = (component: string): boolean => { + return getComponentCodes().includes(component); + }; + + // 여러 권한 중 하나라도 있는지 체크 + const hasAnyPagePermission = (pages: string[]): boolean => { + return pages.some(page => hasPagePermission(page)); + }; + + const hasAnyMenuPermission = (menus: string[]): boolean => { + return menus.some(menu => hasMenuPermission(menu)); + }; + + const hasAnyComponentPermission = (components: string[]): boolean => { + return components.some(component => hasComponentPermission(component)); + }; + + // 계층 구조를 고려한 권한 체크 함수들 + const hasResourcePermission = (resourceCode: string): boolean => { + return ( + findResourceByCode(permissions.value.resources, resourceCode) !== + undefined + ); + }; + + const getResourceByCode = (code: string): Resource | undefined => { + return findResourceByCode(permissions.value.resources, code); + }; + + const getResourceByPath = (path: string): Resource | undefined => { + return findResourceByPath(permissions.value.resources, path); + }; + + const getChildResources = (parentCode: string): Resource[] => { + const parent = findResourceByCode( + permissions.value.resources, + parentCode + ); + return parent?.children || []; + }; + + // 계층 구조에서 리소스 찾기 헬퍼 함수들 + + const findResourceByCode = ( + resources: { + menus: Resource[]; + pages: Resource[]; + components: Resource[]; + }, + code: string + ): Resource | undefined => { + const allResources = [ + ...resources.menus, + ...resources.pages, + ...resources.components, + ]; + for (const resource of allResources) { + if (resource.code === code) return resource; + if (resource.children) { + const found = findResourceByCode( + { menus: [], pages: [], components: resource.children }, + code + ); + if (found) return found; + } + } + return undefined; + }; + + const findResourceByPath = ( + resources: { + menus: Resource[]; + pages: Resource[]; + components: Resource[]; + }, + path: string + ): Resource | undefined => { + const allResources = [ + ...resources.menus, + ...resources.pages, + ...resources.components, + ]; + for (const resource of allResources) { + if (resource.path === path) return resource; + if (resource.children) { + const found = findResourceByPath( + { menus: [], pages: [], components: resource.children }, + path + ); + if (found) return found; + } + } + return undefined; + }; + + // 권한 초기화 + const clearPermissions = () => { + permissions.value = { + resources: { + menus: [], + pages: [], + components: [], + }, + }; + }; + + return { + // 상태 + permissions: readonly(permissions), + isLoading: readonly(isLoading), + + // 액션 + fetchPermissions, + clearPermissions, + + // 기존 호환성 함수들 + hasPagePermission, + hasMenuPermission, + hasComponentPermission, + hasAnyPagePermission, + hasAnyMenuPermission, + hasAnyComponentPermission, + + // 헬퍼 함수들 + getPagePaths, + getMenuCodes, + getComponentCodes, + + // 새로운 계층 구조 권한 체크 함수들 + hasResourcePermission, + getResourceByCode, + getResourceByPath, + getChildResources, + }; + }, + { + persist: true, + } +); diff --git a/stores/user.ts b/stores/user.ts index d5d91f1..859bd02 100644 --- a/stores/user.ts +++ b/stores/user.ts @@ -9,13 +9,14 @@ export const useUserStore = defineStore( } | null>(null); const token = ref(null); - // 추후 제거 필요 - const isAdmin = true; + // 권한 스토어 참조 + const permissionsStore = usePermissionsStore(); interface LoginData { userId: string; name: string; } + const login = async (userId: string, password: string) => { const { success, data, description } = await useApi< ApiResponse @@ -27,6 +28,10 @@ export const useUserStore = defineStore( if (success) { user.value = data; isLoggedIn.value = true; + + // 로그인 성공 시 권한 데이터 가져오기 + await permissionsStore.fetchPermissions(); + return { success, data }; } else { return { @@ -39,21 +44,13 @@ export const useUserStore = defineStore( const logout = () => { user.value = null; isLoggedIn.value = false; + + // 권한 데이터도 초기화 + permissionsStore.clearPermissions(); + useApi("/members/logout", { method: "post" }); }; - const checkAuth = () => { - // 페이지 로드 시 로컬 스토리지에서 사용자 정보 복원 - const savedUser = localStorage.getItem("user"); - const savedToken = localStorage.getItem("token"); - - if (savedUser && savedToken) { - user.value = JSON.parse(savedUser); - token.value = savedToken; - isLoggedIn.value = true; - } - }; - const setToken = (accessToken: string) => { token.value = accessToken; }; @@ -62,24 +59,15 @@ export const useUserStore = defineStore( return token; }; - // 초기 인증 상태 확인 - if (import.meta.client) { - checkAuth(); - } - return { // 상태 isLoggedIn, user, token, - // 게터 - isAdmin, - // 액션 login, logout, - checkAuth, setToken, getToken, }; diff --git a/types/permissions.ts b/types/permissions.ts new file mode 100644 index 0000000..94272ea --- /dev/null +++ b/types/permissions.ts @@ -0,0 +1,345 @@ +/** + * 권한 시스템 타입 정의 + * + * 2자리 계층 코드 시스템을 사용하여 메뉴 > 페이지 > 컴포넌트 계층 구조를 표현합니다. + * + * 코드 규칙: + * - 메뉴: M01, M02, M03, M04... + * - 페이지: P0101 (M01 하위), P0201 (M02 하위), P0501 (독립 페이지)... + * - 컴포넌트: C010101 (P0101 하위), C010102 (P0101 하위)... + */ + +// 리소스 타입 정의 (메뉴, 페이지, 컴포넌트) +export type ResourceType = "menu" | "page" | "component"; + +// 개별 리소스 객체 (계층 구조 지원) +export interface Resource { + oid: number; // OID (고유 식별자) + code: string; // 2자리 계층 코드 (M01, P0101, C010101) + name: string; // 리소스 이름 + parentCode?: string; // 부모 리소스 코드 (계층 구조) + type: ResourceType; // 리소스 타입 + path?: string; // 페이지 경로 (페이지 타입만) + sortOrder: number; // 정렬 순서 + description?: string; // 리소스 설명 + componentType?: string; // 컴포넌트 세부 타입 (버튼, 그리드 등) + children?: Resource[]; // 자식 리소스들 (계층 구조) +} + +// 사용자 권한 구조 (계층적 리소스 관리) +export interface UserPermissions { + resources: { + menus: Resource[]; // 메뉴 리소스들 (M01, M02...) + pages: Resource[]; // 페이지 리소스들 (P0101, P0201...) + components: Resource[]; // 컴포넌트 리소스들 (C010101, C010102...) + }; +} + +// 권한 체크 결과 타입 (권한 스토어에서 제공하는 함수들) +export interface PermissionCheckResult { + // 기존 호환성 함수들 (경로/코드 기반) + hasPagePermission: (page: string) => boolean; // 페이지 경로로 권한 체크 + hasMenuPermission: (menu: string) => boolean; // 메뉴 코드로 권한 체크 + hasComponentPermission: (component: string) => boolean; // 컴포넌트 코드로 권한 체크 + + // 계층 구조를 고려한 권한 체크 함수들 + hasResourcePermission: (resourceCode: string) => boolean; // 리소스 코드로 권한 체크 + getResourceByCode: (code: string) => Resource | undefined; // 코드로 리소스 조회 + getResourceByPath: (path: string) => Resource | undefined; // 경로로 리소스 조회 + getChildResources: (parentCode: string) => Resource[]; // 부모의 자식 리소스들 조회 +} + +// 권한 종류별 상수 (기존 호환성 유지용) +export const PERMISSION_TYPES = { + PAGE: "pagePermissions", + MENU: "menuPermissions", + COMPONENT: "componentPermissions", +} as const; + +/** + * 가데이터용 권한 목록 (개발/테스트용) + * + * 2자리 계층 코드 시스템을 사용한 실제 권한 데이터 예시입니다. + * + * 계층 구조: + * - M01 (관리자 메뉴) > P0101~P0105 (관리자 페이지들) > C010101~C010106 (컴포넌트들) + * - M02 (사용자 메뉴) > P0201~P0202 (사용자 페이지들) + * - P0501~P0504 (독립 페이지들: 홈, 로그인, 회원가입, 소개) + * - P0601~P0602 (테스트 페이지들) + * - P0701 (팝업 페이지들) + */ +export const MOCK_PERMISSIONS: UserPermissions = { + resources: { + // 메뉴 리소스들 (최상위 레벨) + menus: [ + { + oid: 1, + code: "M01", + name: "테스트 메뉴", + type: "menu", + sortOrder: 1, + description: "테스트 관련 메뉴", + }, + { + oid: 2, + code: "M02", + name: "관리자 메뉴", + type: "menu", + sortOrder: 2, + description: "관리자 전용 메뉴", + }, + ], + + // 페이지 리소스들 (화면 메뉴 구성에 맞춤) + pages: [ + // 테스트 메뉴 하위 페이지들 (M01 > P0101~P0115) + { + oid: 5, + code: "P0101", + name: "테스트", + type: "page", + path: "/test", + parentCode: "M01", + sortOrder: 1, + description: "기본 테스트 페이지", + }, + { + oid: 6, + code: "P0102", + name: "ivg", + type: "page", + path: "/test/igv", + parentCode: "M01", + sortOrder: 2, + description: "IGV 테스트 페이지", + }, + { + oid: 7, + code: "P0103", + name: "ivg2", + type: "page", + path: "/test/igv2", + parentCode: "M01", + sortOrder: 3, + description: "IGV2 테스트 페이지", + }, + { + oid: 8, + code: "P0104", + name: "pathway", + type: "page", + path: "/test/pathway", + parentCode: "M01", + sortOrder: 4, + description: "경로 분석 페이지", + }, + { + oid: 9, + code: "P0105", + name: "pathway2", + type: "page", + path: "/test/pathway2", + parentCode: "M01", + sortOrder: 5, + description: "경로 분석 페이지 2", + }, + { + oid: 10, + code: "P0106", + name: "pathway3", + type: "page", + path: "/test/pathway3", + parentCode: "M01", + sortOrder: 6, + description: "경로 분석 페이지 3", + }, + { + oid: 11, + code: "P0107", + name: "pathway4", + type: "page", + path: "/test/pathway4", + parentCode: "M01", + sortOrder: 7, + description: "경로 분석 페이지 4", + }, + { + oid: 12, + code: "P0108", + name: "pathwayjson", + type: "page", + path: "/test/pathwayJson", + parentCode: "M01", + sortOrder: 8, + description: "경로 분석 JSON 페이지", + }, + { + oid: 13, + code: "P0109", + name: "배양그래프", + type: "page", + path: "/test/culture-graph", + parentCode: "M01", + sortOrder: 9, + description: "배양 그래프 페이지", + }, + { + oid: 14, + code: "P0110", + name: "배양그래프 멀티", + type: "page", + path: "/test/culture-graph-multi", + parentCode: "M01", + sortOrder: 10, + description: "배양 그래프 멀티 페이지", + }, + { + oid: 15, + code: "P0111", + name: "배양그래프 탭", + type: "page", + path: "/test/culture-graph-tab", + parentCode: "M01", + sortOrder: 11, + description: "배양 그래프 탭 페이지", + }, + { + oid: 16, + code: "P0112", + name: "tui-grid", + type: "page", + path: "/tui", + parentCode: "M01", + sortOrder: 12, + description: "TUI 그리드 페이지", + }, + { + oid: 17, + code: "P0113", + name: "리소스", + type: "page", + path: "/admin/resource", + parentCode: "M01", + sortOrder: 13, + description: "리소스 관리 페이지", + }, + { + oid: 18, + code: "P0114", + name: "sample", + type: "page", + path: "/sampleList", + parentCode: "M01", + sortOrder: 14, + description: "샘플 목록 페이지", + }, + { + oid: 19, + code: "P0115", + name: "공용 기능 테스트", + type: "page", + path: "/test/common-test", + parentCode: "M01", + sortOrder: 15, + description: "공용 기능 테스트 페이지", + }, + + // 관리자 메뉴 하위 페이지들 (M02 > P0201~P0203) + { + oid: 20, + code: "P0201", + name: "접속기록", + type: "page", + path: "/admin/logs", + parentCode: "M02", + sortOrder: 1, + description: "사용자 접속 기록 관리", + }, + { + oid: 21, + code: "P0202", + name: "공통코드", + type: "page", + path: "/admin/codes", + parentCode: "M02", + sortOrder: 2, + description: "공통 코드 관리", + }, + { + oid: 22, + code: "P0203", + name: "프로그램", + type: "page", + path: "/admin/programs", + parentCode: "M02", + sortOrder: 3, + description: "프로그램 관리", + }, + ], + + // 컴포넌트 리소스들 (페이지 하위) + components: [ + // 테스트 페이지 하위 컴포넌트들 (P0101 > C010101~C010106) + { + oid: 19, + code: "C010101", + name: "삭제 버튼", + type: "component", + componentType: "button", + parentCode: "P0101", + sortOrder: 1, + description: "테스트 삭제 버튼", + }, + { + oid: 20, + code: "C010102", + name: "수정 버튼", + type: "component", + componentType: "button", + parentCode: "P0101", + sortOrder: 2, + description: "테스트 수정 버튼", + }, + { + oid: 21, + code: "C010103", + name: "내보내기 버튼", + type: "component", + componentType: "button", + parentCode: "P0101", + sortOrder: 3, + description: "테스트 내보내기 버튼", + }, + { + oid: 22, + code: "C010104", + name: "가져오기 버튼", + type: "component", + componentType: "button", + parentCode: "P0101", + sortOrder: 4, + description: "테스트 가져오기 버튼", + }, + { + oid: 23, + code: "C010105", + name: "생성 버튼", + type: "component", + componentType: "button", + parentCode: "P0101", + sortOrder: 5, + description: "테스트 생성 버튼", + }, + { + oid: 24, + code: "C010106", + name: "보기 버튼", + type: "component", + componentType: "button", + parentCode: "P0101", + sortOrder: 6, + description: "테스트 상세보기 버튼", + }, + ], + }, +};