[메뉴 권한 1차 작업]
This commit is contained in:
@@ -7,26 +7,20 @@
|
|||||||
<button
|
<button
|
||||||
class="menu-btn"
|
class="menu-btn"
|
||||||
:class="{ active: modelValue === 'home' }"
|
:class="{ active: modelValue === 'home' }"
|
||||||
@click="$emit('update:modelValue', 'home')"
|
@click="onMenuClick('home')"
|
||||||
>
|
>
|
||||||
HOME
|
HOME
|
||||||
</button>
|
</button>
|
||||||
<!-- 테스트트 메뉴 -->
|
|
||||||
|
<!-- 권한 기반 메뉴 -->
|
||||||
<button
|
<button
|
||||||
|
v-for="menu in availableMenus"
|
||||||
|
:key="menu.code"
|
||||||
class="menu-btn"
|
class="menu-btn"
|
||||||
:class="{ active: modelValue === 'test' }"
|
:class="{ active: modelValue === menu.code }"
|
||||||
@click="$emit('update:modelValue', 'test')"
|
@click="onMenuClick(menu.code)"
|
||||||
>
|
>
|
||||||
테스트 메뉴
|
{{ menu.name }}
|
||||||
</button>
|
|
||||||
<!-- 관리자 메뉴 (관리자만 표시) -->
|
|
||||||
<button
|
|
||||||
v-if="userStore.isAdmin"
|
|
||||||
class="menu-btn"
|
|
||||||
:class="{ active: modelValue === 'admin' }"
|
|
||||||
@click="$emit('update:modelValue', 'admin')"
|
|
||||||
>
|
|
||||||
관리자 메뉴
|
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -77,22 +71,35 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useUserStore } from "~/stores/user";
|
import { useUserStore } from "~/stores/user";
|
||||||
|
import { usePermissionsStore } from "~/stores/permissions";
|
||||||
|
|
||||||
defineProps({
|
// defineModel 사용
|
||||||
modelValue: {
|
const modelValue = defineModel({ type: String, required: true });
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
defineEmits(["update:modelValue"]);
|
|
||||||
|
|
||||||
const showDropdown = ref(false);
|
const showDropdown = ref(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const permissionStore = usePermissionsStore();
|
||||||
|
|
||||||
|
// 권한 초기화
|
||||||
|
onMounted(async () => {
|
||||||
|
await permissionStore.fetchPermissions();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 권한이 있는 메뉴들만 필터링
|
||||||
|
const availableMenus = computed(() => {
|
||||||
|
return permissionStore.permissions.resources.menus.filter(menu => {
|
||||||
|
return permissionStore.hasMenuPermission(menu.code);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 메뉴 클릭 핸들러
|
||||||
|
function onMenuClick(menu) {
|
||||||
|
modelValue.value = menu;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleDropdown() {
|
function toggleDropdown() {
|
||||||
showDropdown.value = !showDropdown.value;
|
showDropdown.value = !showDropdown.value;
|
||||||
|
|||||||
43
composables/usePermission.ts
Normal file
43
composables/usePermission.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 권한 체크를 위한 컴포저블
|
||||||
|
* 컴포넌트에서 권한을 쉽게 체크할 수 있도록 도와주는 유틸리티 함수들
|
||||||
|
*/
|
||||||
|
export const usePermission = () => {
|
||||||
|
const permissionsStore = usePermissionsStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 페이지 권한 체크
|
||||||
|
hasPagePermission: (page: string) =>
|
||||||
|
permissionsStore.hasPagePermission(page),
|
||||||
|
|
||||||
|
// 메뉴 권한 체크
|
||||||
|
hasMenuPermission: (menu: string) =>
|
||||||
|
permissionsStore.hasMenuPermission(menu),
|
||||||
|
|
||||||
|
// 컴포넌트 권한 체크
|
||||||
|
hasComponentPermission: (component: string) =>
|
||||||
|
permissionsStore.hasComponentPermission(component),
|
||||||
|
|
||||||
|
// 여러 권한 중 하나라도 있는지 체크
|
||||||
|
hasAnyPagePermission: (pages: string[]) =>
|
||||||
|
permissionsStore.hasAnyPagePermission(pages),
|
||||||
|
hasAnyMenuPermission: (menus: string[]) =>
|
||||||
|
permissionsStore.hasAnyMenuPermission(menus),
|
||||||
|
hasAnyComponentPermission: (components: string[]) =>
|
||||||
|
permissionsStore.hasAnyComponentPermission(components),
|
||||||
|
|
||||||
|
// 모든 권한이 있는지 체크
|
||||||
|
hasAllPagePermissions: (pages: string[]) =>
|
||||||
|
pages.every(page => permissionsStore.hasPagePermission(page)),
|
||||||
|
hasAllMenuPermissions: (menus: string[]) =>
|
||||||
|
menus.every(menu => permissionsStore.hasMenuPermission(menu)),
|
||||||
|
hasAllComponentPermissions: (components: string[]) =>
|
||||||
|
components.every(component =>
|
||||||
|
permissionsStore.hasComponentPermission(component)
|
||||||
|
),
|
||||||
|
|
||||||
|
// 권한 데이터 직접 접근
|
||||||
|
permissions: permissionsStore.permissions,
|
||||||
|
isLoading: permissionsStore.isLoading,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,125 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppHeader from "../components/layout/AppHeader.vue";
|
import AppHeader from "../components/layout/AppHeader.vue";
|
||||||
import { ref, computed, watch } from "vue";
|
import { ref, computed, watch, onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useTabsStore } from "../stores/tab";
|
import { useTabsStore } from "../stores/tab";
|
||||||
|
import { usePermissionsStore } from "~/stores/permissions";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const activeMenu = ref("home");
|
const activeMenu = ref("HOME");
|
||||||
const showSubmenuBar = ref(false);
|
const showSubmenuBar = ref(false);
|
||||||
|
|
||||||
const tabsStore = useTabsStore();
|
const tabsStore = useTabsStore();
|
||||||
|
const permissionStore = usePermissionsStore();
|
||||||
|
|
||||||
|
// 권한 초기화
|
||||||
|
onMounted(async () => {
|
||||||
|
await permissionStore.fetchPermissions();
|
||||||
|
});
|
||||||
|
|
||||||
// 메뉴 클릭 시 홈 이동
|
// 메뉴 클릭 시 홈 이동
|
||||||
watch(activeMenu, newValue => {
|
watch(activeMenu, newValue => {
|
||||||
if (newValue === "home") router.push("/");
|
if (newValue === "HOME") router.push("/");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 서브메뉴 정의
|
// 권한 기반 서브메뉴 생성
|
||||||
const subMenus = computed(() => {
|
const subMenus = computed(() => {
|
||||||
if (activeMenu.value === "test") {
|
if (activeMenu.value === "HOME") return [];
|
||||||
return [
|
|
||||||
{
|
// 활성 메뉴의 코드 찾기 (M01, M02 등)
|
||||||
key: "test",
|
const activeMenuCode = activeMenu.value;
|
||||||
label: "테스트",
|
|
||||||
to: "/test/test01",
|
// 해당 메뉴의 하위 페이지들 필터링
|
||||||
},
|
return permissionStore.permissions.resources.pages
|
||||||
{
|
.filter(page => page.parentCode === activeMenuCode)
|
||||||
key: "igv",
|
.filter(page => permissionStore.hasPagePermission(page.path || ""))
|
||||||
label: "ivg",
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
to: "/test/test02",
|
.map(page => ({
|
||||||
},
|
key: page.code,
|
||||||
{
|
label: page.name,
|
||||||
key: "igv2",
|
to: page.path || "",
|
||||||
label: "ivg2",
|
componentName: page.name,
|
||||||
to: "/test/igv2",
|
}));
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "pathway",
|
|
||||||
label: "pathway",
|
|
||||||
to: "/test/pathway",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "pathway2",
|
|
||||||
label: "pathway2",
|
|
||||||
to: "/test/pathway2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "pathway3",
|
|
||||||
label: "pathway3",
|
|
||||||
to: "/test/pathway3",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "pathway4",
|
|
||||||
label: "pathway4",
|
|
||||||
to: "/cultureGraph/pathway4",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "pathwayjson",
|
|
||||||
label: "pathwayjson",
|
|
||||||
to: "/test/pathwayjson",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "cultureGraph",
|
|
||||||
label: "배양그래프",
|
|
||||||
to: "/test/culture-graph",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "cultureGraphMulti",
|
|
||||||
label: "배양그래프 멀티",
|
|
||||||
to: "/test/culture-graph-multi",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "cultureGraphTab",
|
|
||||||
label: "배양그래프 탭",
|
|
||||||
to: "/test/culture-graph-tab",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "tui-grid",
|
|
||||||
label: "tui-grid",
|
|
||||||
to: "/tui",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "리소스",
|
|
||||||
label: "리소스",
|
|
||||||
to: "/admin/resource",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "sample",
|
|
||||||
label: "sample",
|
|
||||||
to: "/sampleList",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "common-test",
|
|
||||||
label: "공용 기능 테스트",
|
|
||||||
to: "/common-test",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else if (activeMenu.value === "ADMIN") {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: "logs",
|
|
||||||
label: "접속기록",
|
|
||||||
to: "/admin/logs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "codes",
|
|
||||||
label: "공통코드",
|
|
||||||
to: "/admin/codes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "programs",
|
|
||||||
label: "프로그램",
|
|
||||||
to: "/admin/programs",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function onMenuClick(menu: string) {
|
function onMenuClick(menu: string) {
|
||||||
activeMenu.value = menu;
|
showSubmenuBar.value = menu !== "home";
|
||||||
showSubmenuBar.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 서브메뉴 클릭 → 현재 활성 탭 내용만 변경
|
// ✅ 서브메뉴 클릭 → 현재 활성 탭 내용만 변경
|
||||||
@@ -147,18 +71,24 @@ function addNewTab() {
|
|||||||
|
|
||||||
<!-- 서브메뉴 바 -->
|
<!-- 서브메뉴 바 -->
|
||||||
<nav
|
<nav
|
||||||
v-if="subMenus && subMenus.length && showSubmenuBar"
|
v-if="showSubmenuBar && subMenus.length > 0"
|
||||||
class="submenu-bar"
|
class="w-full bg-gray-100 shadow-sm px-4 py-2"
|
||||||
@click.stop
|
|
||||||
>
|
>
|
||||||
|
<div class="flex items-center space-x-6">
|
||||||
|
<span class="text-sm font-medium text-gray-600 mr-4">
|
||||||
|
{{ activeMenu }}
|
||||||
|
</span>
|
||||||
|
<div class="flex space-x-4">
|
||||||
<button
|
<button
|
||||||
v-for="sub in subMenus"
|
v-for="sub in subMenus"
|
||||||
:key="sub.key"
|
:key="sub.key"
|
||||||
class="submenu-btn"
|
class="submenu-btn"
|
||||||
@click="onSubMenuClick({ ...sub, componentName: sub.key })"
|
@click="onSubMenuClick(sub)"
|
||||||
>
|
>
|
||||||
{{ sub.label }}
|
{{ sub.label }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<!-- 탭 바 -->
|
<!-- 탭 바 -->
|
||||||
@@ -221,25 +151,19 @@ function addNewTab() {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.submenu-btn {
|
.submenu-btn {
|
||||||
font-size: 1.05rem;
|
padding: 0.25rem 0.75rem;
|
||||||
font-weight: 500;
|
font-size: 0.875rem;
|
||||||
color: #222;
|
color: #374151;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.5rem 1.2rem;
|
border-radius: 0.25rem;
|
||||||
border-radius: 6px;
|
|
||||||
transition:
|
|
||||||
background 0.15s,
|
|
||||||
color 0.15s;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
.submenu-btn.active {
|
|
||||||
background: none;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
.submenu-btn:hover {
|
.submenu-btn:hover {
|
||||||
background: #e6f0fa;
|
color: #2563eb;
|
||||||
color: #1976d2;
|
background-color: #eff6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 탭바 스타일 */
|
/* 탭바 스타일 */
|
||||||
|
|||||||
@@ -2,26 +2,29 @@ export default defineNuxtRouteMiddleware((to, _from) => {
|
|||||||
// 클라이언트 사이드에서만 실행
|
// 클라이언트 사이드에서만 실행
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const permissionsStore = usePermissionsStore();
|
||||||
|
|
||||||
// 보호된 라우트 목록(메뉴 확정되면 수정)
|
// 보호된 라우트 목록(메뉴 확정되면 수정)
|
||||||
const protectedRoutes = ["/admin", "/profile", "/dashboard"];
|
const protectedRoutes = ["/admin", "/profile", "/dashboard"];
|
||||||
|
|
||||||
// 현재 라우트가 보호된 라우트인지 확인
|
// 현재 라우트가 보호된 라우트인지 확인
|
||||||
const isProtectedRoute = protectedRoutes.some((route) =>
|
const isProtectedRoute = protectedRoutes.some(route =>
|
||||||
to.path.startsWith(route)
|
to.path.startsWith(route)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 관리자 전용 라우트 확인
|
// 로그인 체크
|
||||||
const isAdminRoute = to.path.startsWith("/admin");
|
|
||||||
|
|
||||||
if (isProtectedRoute && !userStore.isLoggedIn) {
|
if (isProtectedRoute && !userStore.isLoggedIn) {
|
||||||
// 인증되지 않은 사용자를 로그인 페이지로 리다이렉트
|
// 인증되지 않은 사용자를 로그인 페이지로 리다이렉트
|
||||||
return navigateTo("/login");
|
return navigateTo("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdminRoute && !userStore.isAdmin) {
|
// API 권한 체크 (로그인된 사용자만)
|
||||||
// 관리자가 아닌 사용자를 홈 페이지로 리다이렉트
|
if (userStore.isLoggedIn) {
|
||||||
|
const currentPath = to.path;
|
||||||
|
if (!permissionsStore.hasApiPermission(currentPath)) {
|
||||||
|
// 권한이 없는 경우 홈으로 리다이렉트
|
||||||
return navigateTo("/");
|
return navigateTo("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4"
|
|
||||||
>
|
|
||||||
<div class="max-w-6xl mx-auto">
|
|
||||||
<!-- 페이지 헤더 -->
|
|
||||||
<div class="text-center mb-8">
|
|
||||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">공용 기능 테스트</h1>
|
|
||||||
<p class="text-xl text-gray-600">
|
|
||||||
API 및 공용 기능들의 동작을 테스트할 수 있습니다
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API 테스트 섹션 -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
|
||||||
<!-- 좌측: 자동 에러 처리 테스트 -->
|
|
||||||
<div class="lg:col-span-2 space-y-6">
|
|
||||||
<!-- 자동 에러 처리 테스트 -->
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
|
||||||
<div class="flex items-center mb-4">
|
|
||||||
<div
|
|
||||||
class="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center mr-3"
|
|
||||||
>
|
|
||||||
<span class="text-white font-bold text-sm">1</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900">
|
|
||||||
자동 에러 처리 테스트
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600 mb-4">
|
|
||||||
useApi 함수의 자동 에러 처리 기능을 테스트합니다. 에러가 발생하면
|
|
||||||
자동으로 alert가 표시됩니다.
|
|
||||||
</p>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<button
|
|
||||||
class="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white font-medium py-3 px-4 rounded-lg transition-colors"
|
|
||||||
:disabled="isLoading"
|
|
||||||
@click="apiTest"
|
|
||||||
>
|
|
||||||
{{ isLoading ? "테스트 중..." : "자동 에러 처리 테스트" }}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="autoErrorResult"
|
|
||||||
class="mt-3 p-3 bg-gray-50 rounded border"
|
|
||||||
>
|
|
||||||
<h4 class="font-medium text-gray-900 mb-2">결과:</h4>
|
|
||||||
<pre class="text-sm text-gray-700 whitespace-pre-wrap">{{
|
|
||||||
autoErrorResult
|
|
||||||
}}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 예제 소스 -->
|
|
||||||
<div class="mt-4 p-3 bg-blue-50 rounded border">
|
|
||||||
<h5 class="font-medium text-blue-900 mb-2">예제 소스:</h5>
|
|
||||||
<pre class="text-xs text-blue-800 whitespace-pre-wrap">
|
|
||||||
{`// 자동 에러 처리 예제
|
|
||||||
const apiTest = async () => {
|
|
||||||
const response = await useApi<ApiResponse<object>>(
|
|
||||||
"/admin/common-codes/USER_STATUS_ACTIVE222"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
console.log("response:", response);
|
|
||||||
}
|
|
||||||
};`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 직접 에러 처리 테스트 -->
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
|
||||||
<div class="flex items-center mb-4">
|
|
||||||
<div
|
|
||||||
class="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center mr-3"
|
|
||||||
>
|
|
||||||
<span class="text-white font-bold text-sm">2</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900">
|
|
||||||
직접 에러 처리 테스트
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600 mb-4">
|
|
||||||
useApi 함수의 직접 에러 처리 기능을 테스트합니다. 에러 타입에 따른
|
|
||||||
세밀한 처리를 확인할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<button
|
|
||||||
class="w-full bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white font-medium py-3 px-4 rounded-lg transition-colors"
|
|
||||||
:disabled="isLoadingCustom"
|
|
||||||
@click="apiTestWithCustomError"
|
|
||||||
>
|
|
||||||
{{ isLoadingCustom ? "테스트 중..." : "직접 에러 처리 테스트" }}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="customErrorResult"
|
|
||||||
class="mt-3 p-3 bg-gray-50 rounded border"
|
|
||||||
>
|
|
||||||
<h4 class="font-medium text-gray-900 mb-2">결과:</h4>
|
|
||||||
<pre class="text-sm text-gray-700 whitespace-pre-wrap">{{
|
|
||||||
customErrorResult
|
|
||||||
}}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 예제 소스 -->
|
|
||||||
<div class="mt-4 p-3 bg-green-50 rounded border">
|
|
||||||
<h5 class="font-medium text-green-900 mb-2">예제 소스:</h5>
|
|
||||||
<pre class="text-xs text-green-800 whitespace-pre-wrap">
|
|
||||||
{`// 직접 에러 처리 예제
|
|
||||||
const apiTestWithCustomError = async () => {
|
|
||||||
try {
|
|
||||||
const response = await useApi<ApiResponse<object>>(
|
|
||||||
"/admin/common-codes/USER_STATUS_ACTIVE222",
|
|
||||||
{
|
|
||||||
handleError: false, // 에러를 직접 처리
|
|
||||||
showAlert: false, // alert 표시 안함
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
console.log("response:", response);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// 에러 타입에 따른 세밀한 처리
|
|
||||||
if (error.response?.status === 404) {
|
|
||||||
alert("요청한 코드를 찾을 수 없습니다.");
|
|
||||||
} else if (error.response?.status === 403) {
|
|
||||||
alert("접근 권한이 없습니다.");
|
|
||||||
} else if (error.response?.status >= 500) {
|
|
||||||
alert("서버 오류가 발생했습니다.");
|
|
||||||
} else {
|
|
||||||
alert("알 수 없는 오류가 발생했습니다.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 우측: 추가 API 테스트 -->
|
|
||||||
<div class="lg:col-span-1">
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
|
||||||
<div class="flex items-center mb-4">
|
|
||||||
<div
|
|
||||||
class="w-10 h-10 bg-purple-500 rounded-full flex items-center justify-center mr-3"
|
|
||||||
>
|
|
||||||
<span class="text-white font-bold text-sm">3</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900">
|
|
||||||
추가 API 테스트
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600 mb-4">
|
|
||||||
다양한 API 엔드포인트를 테스트할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
|
||||||
<button
|
|
||||||
class="w-full bg-purple-500 hover:bg-purple-600 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
:disabled="isLoadingValid"
|
|
||||||
@click="testValidEndpoint"
|
|
||||||
>
|
|
||||||
{{ isLoadingValid ? "테스트 중..." : "유효한 엔드포인트" }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full bg-red-500 hover:bg-red-600 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
:disabled="isLoadingNetwork"
|
|
||||||
@click="testNetworkError"
|
|
||||||
>
|
|
||||||
{{ isLoadingNetwork ? "테스트 중..." : "네트워크 에러" }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
@click="clearResults"
|
|
||||||
>
|
|
||||||
결과 초기화
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="additionalResults.length > 0" class="mt-4 space-y-3">
|
|
||||||
<h4 class="font-medium text-gray-900">추가 테스트 결과:</h4>
|
|
||||||
<div
|
|
||||||
v-for="(result, index) in additionalResults"
|
|
||||||
:key="index"
|
|
||||||
class="p-3 bg-gray-50 rounded border"
|
|
||||||
>
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<span class="font-medium text-gray-900 text-sm">{{
|
|
||||||
result.title
|
|
||||||
}}</span>
|
|
||||||
<span class="text-xs text-gray-500">{{
|
|
||||||
result.timestamp
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<pre class="text-xs text-gray-700 whitespace-pre-wrap">{{
|
|
||||||
result.data
|
|
||||||
}}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 예제 소스 -->
|
|
||||||
<div class="mt-4 p-3 bg-purple-50 rounded border">
|
|
||||||
<h5 class="font-medium text-purple-900 mb-2">예제 소스:</h5>
|
|
||||||
<pre class="text-xs text-purple-800 whitespace-pre-wrap">
|
|
||||||
{`// 추가 API 테스트 예제
|
|
||||||
const testValidEndpoint = async () => {
|
|
||||||
try {
|
|
||||||
const response = await useApi<ApiResponse<object>>(
|
|
||||||
"/admin/common-codes/USER_STATUS_ACTIVE",
|
|
||||||
{
|
|
||||||
handleError: false,
|
|
||||||
showAlert: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("성공:", response);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log("에러:", error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testNetworkError = async () => {
|
|
||||||
try {
|
|
||||||
const response = await useApi<ApiResponse<object>>(
|
|
||||||
"/non-existent-endpoint",
|
|
||||||
{
|
|
||||||
handleError: false,
|
|
||||||
showAlert: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log("네트워크 에러:", error.message);
|
|
||||||
}
|
|
||||||
};`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 사용법 가이드 -->
|
|
||||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-yellow-800 mb-3">
|
|
||||||
사용법 가이드
|
|
||||||
</h3>
|
|
||||||
<div class="text-yellow-700 space-y-2">
|
|
||||||
<p>
|
|
||||||
<strong>자동 에러 처리:</strong> useApi 함수가 에러를 자동으로
|
|
||||||
처리하고 사용자에게 알림을 표시합니다.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>직접 에러 처리:</strong> handleError: false 옵션을 사용하여
|
|
||||||
에러를 직접 처리할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>에러 타입:</strong> 404 (Not Found), 403 (Forbidden), 500+
|
|
||||||
(Server Error) 등 다양한 에러 상황을 테스트할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// import { useUserStore } from "~/stores/user"; // 현재 사용하지 않음
|
|
||||||
|
|
||||||
// 페이지 메타데이터 설정
|
|
||||||
definePageMeta({
|
|
||||||
title: "공용 기능 테스트",
|
|
||||||
description: "API 및 공용 기능들의 동작을 테스트하는 페이지",
|
|
||||||
});
|
|
||||||
|
|
||||||
// const userStore = useUserStore(); // 현재 사용하지 않음
|
|
||||||
|
|
||||||
// 반응형 데이터
|
|
||||||
const isLoading = ref(false);
|
|
||||||
const isLoadingCustom = ref(false);
|
|
||||||
const isLoadingValid = ref(false);
|
|
||||||
const isLoadingNetwork = ref(false);
|
|
||||||
const autoErrorResult = ref("");
|
|
||||||
const customErrorResult = ref("");
|
|
||||||
const additionalResults = ref<
|
|
||||||
Array<{ title: string; data: string; timestamp: string }>
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
// 테스트 다운로드 함수 (자동 에러 처리)
|
|
||||||
const apiTest = async () => {
|
|
||||||
isLoading.value = true;
|
|
||||||
autoErrorResult.value = "";
|
|
||||||
|
|
||||||
const response = await useApi<ApiResponse<object>>(
|
|
||||||
"/admin/common-codes/USER_STATUS_ACTIVE222"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
autoErrorResult.value = `성공: ${JSON.stringify(response, null, 2)}`;
|
|
||||||
} else {
|
|
||||||
autoErrorResult.value = "응답이 없습니다.";
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 직접 에러 처리하는 함수 예시
|
|
||||||
const apiTestWithCustomError = async () => {
|
|
||||||
isLoadingCustom.value = true;
|
|
||||||
customErrorResult.value = "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await useApi<ApiResponse<object>>(
|
|
||||||
"/admin/common-codes/USER_STATUS_ACTIVE222",
|
|
||||||
{
|
|
||||||
handleError: false, // 에러를 직접 처리하겠다는 의미
|
|
||||||
showAlert: false, // alert는 표시하지 않음
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
customErrorResult.value = `성공: ${JSON.stringify(response, null, 2)}`;
|
|
||||||
} else {
|
|
||||||
customErrorResult.value = "응답이 없습니다.";
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// 에러 타입에 처리
|
|
||||||
let errorMessage = "";
|
|
||||||
if (error.response?.status === 404) {
|
|
||||||
errorMessage = "[errorCustomHandler]요청한 코드를 찾을 수 없습니다.";
|
|
||||||
} else if (error.response?.status === 403) {
|
|
||||||
errorMessage = "[errorCustomHandler]접근 권한이 없습니다.";
|
|
||||||
} else if (error.response?.status >= 500) {
|
|
||||||
errorMessage =
|
|
||||||
"[errorCustomHandler]서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.";
|
|
||||||
} else {
|
|
||||||
errorMessage = "[errorCustomHandler]알 수 없는 오류가 발생했습니다.";
|
|
||||||
}
|
|
||||||
|
|
||||||
customErrorResult.value = `에러 처리됨: ${errorMessage}\n상세 정보: ${JSON.stringify(error.response?.data || error.message, null, 2)}`;
|
|
||||||
} finally {
|
|
||||||
isLoadingCustom.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 유효한 엔드포인트 테스트
|
|
||||||
const testValidEndpoint = async () => {
|
|
||||||
isLoadingValid.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await useApi<ApiResponse<object>>(
|
|
||||||
"/admin/common-codes/USER_STATUS_ACTIVE",
|
|
||||||
{
|
|
||||||
handleError: false,
|
|
||||||
showAlert: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
additionalResults.value.unshift({
|
|
||||||
title: "유효한 엔드포인트 테스트",
|
|
||||||
data: response ? JSON.stringify(response, null, 2) : "응답 없음",
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
additionalResults.value.unshift({
|
|
||||||
title: "유효한 엔드포인트 테스트 (에러)",
|
|
||||||
data: `에러: ${error.message || "알 수 없는 에러"}`,
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
isLoadingValid.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 네트워크 에러 테스트
|
|
||||||
const testNetworkError = async () => {
|
|
||||||
isLoadingNetwork.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await useApi<ApiResponse<object>>(
|
|
||||||
"/non-existent-endpoint",
|
|
||||||
{
|
|
||||||
handleError: false,
|
|
||||||
showAlert: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
additionalResults.value.unshift({
|
|
||||||
title: "네트워크 에러 테스트",
|
|
||||||
data: response ? JSON.stringify(response, null, 2) : "응답 없음",
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
additionalResults.value.unshift({
|
|
||||||
title: "네트워크 에러 테스트 (에러)",
|
|
||||||
data: `에러: ${error.message || "알 수 없는 에러"}\n상태 코드: ${error.response?.status || "N/A"}`,
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
isLoadingNetwork.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 결과 초기화
|
|
||||||
const clearResults = () => {
|
|
||||||
autoErrorResult.value = "";
|
|
||||||
customErrorResult.value = "";
|
|
||||||
additionalResults.value = [];
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 추가 스타일이 필요한 경우 여기에 작성 */
|
|
||||||
</style>
|
|
||||||
497
pages/[tabId]/test/common-test.vue
Normal file
497
pages/[tabId]/test/common-test.vue
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4"
|
||||||
|
>
|
||||||
|
<div class="max-w-6xl mx-auto">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900 mb-4">공용 기능 테스트</h1>
|
||||||
|
<p class="text-xl text-gray-600">
|
||||||
|
API 및 공용 기능들의 동작을 테스트할 수 있습니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API 테스트 섹션 -->
|
||||||
|
<div class="mb-12">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900 mb-2">API 테스트</h2>
|
||||||
|
<p class="text-lg text-gray-600">
|
||||||
|
useApi 함수의 다양한 기능을 테스트합니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<!-- 좌측: 자동 에러 처리 테스트 -->
|
||||||
|
<div class="lg:col-span-2 space-y-6">
|
||||||
|
<!-- 자동 에러 처리 테스트 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center mr-3"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-sm">1</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
자동 에러 처리 테스트
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
useApi 함수의 자동 에러 처리 기능을 테스트합니다. 에러가
|
||||||
|
발생하면 자동으로 alert가 표시됩니다.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<button
|
||||||
|
class="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white font-medium py-3 px-4 rounded-lg transition-colors"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="apiTest"
|
||||||
|
>
|
||||||
|
{{ isLoading ? "테스트 중..." : "자동 에러 처리 테스트" }}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="autoErrorResult"
|
||||||
|
class="mt-3 p-3 bg-gray-50 rounded border"
|
||||||
|
>
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2">결과:</h4>
|
||||||
|
<pre class="text-sm text-gray-700 whitespace-pre-wrap">{{
|
||||||
|
autoErrorResult
|
||||||
|
}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 예제 소스 -->
|
||||||
|
<div class="mt-4 p-3 bg-blue-50 rounded border">
|
||||||
|
<h5 class="font-medium text-blue-900 mb-2">예제 소스:</h5>
|
||||||
|
<pre class="text-xs text-blue-800 whitespace-pre-wrap">
|
||||||
|
{`// 자동 에러 처리 예제
|
||||||
|
const apiTest = async () => {
|
||||||
|
const response = await useApi<ApiResponse<object>>(
|
||||||
|
"/admin/common-codes/USER_STATUS_ACTIVE222"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
console.log("response:", response);
|
||||||
|
}
|
||||||
|
};`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 직접 에러 처리 테스트 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center mr-3"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-sm">2</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
직접 에러 처리 테스트
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
useApi 함수의 직접 에러 처리 기능을 테스트합니다. 에러 타입에
|
||||||
|
따른 세밀한 처리를 확인할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<button
|
||||||
|
class="w-full bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white font-medium py-3 px-4 rounded-lg transition-colors"
|
||||||
|
:disabled="isLoadingCustom"
|
||||||
|
@click="apiTestWithCustomError"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
isLoadingCustom ? "테스트 중..." : "직접 에러 처리 테스트"
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="customErrorResult"
|
||||||
|
class="mt-3 p-3 bg-gray-50 rounded border"
|
||||||
|
>
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2">결과:</h4>
|
||||||
|
<pre class="text-sm text-gray-700 whitespace-pre-wrap">{{
|
||||||
|
customErrorResult
|
||||||
|
}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 예제 소스 -->
|
||||||
|
<div class="mt-4 p-3 bg-green-50 rounded border">
|
||||||
|
<h5 class="font-medium text-green-900 mb-2">예제 소스:</h5>
|
||||||
|
<pre class="text-xs text-green-800 whitespace-pre-wrap">
|
||||||
|
{`// 직접 에러 처리 예제
|
||||||
|
const apiTestWithCustomError = async () => {
|
||||||
|
try {
|
||||||
|
const response = await useApi<ApiResponse<object>>(
|
||||||
|
"/admin/common-codes/USER_STATUS_ACTIVE222",
|
||||||
|
{
|
||||||
|
handleError: false, // 에러를 직접 처리
|
||||||
|
showAlert: false, // alert 표시 안함
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
console.log("response:", response);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// 에러 타입에 따른 세밀한 처리
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
alert("요청한 코드를 찾을 수 없습니다.");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
alert("접근 권한이 없습니다.");
|
||||||
|
} else if (error.response?.status >= 500) {
|
||||||
|
alert("서버 오류가 발생했습니다.");
|
||||||
|
} else {
|
||||||
|
alert("알 수 없는 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 우측: 추가 API 테스트 -->
|
||||||
|
<div class="lg:col-span-1">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-purple-500 rounded-full flex items-center justify-center mr-3"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-sm">3</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
추가 API 테스트
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
다양한 API 엔드포인트를 테스트할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<button
|
||||||
|
class="w-full bg-purple-500 hover:bg-purple-600 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||||
|
:disabled="isLoadingValid"
|
||||||
|
@click="testValidEndpoint"
|
||||||
|
>
|
||||||
|
{{ isLoadingValid ? "테스트 중..." : "유효한 엔드포인트" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full bg-red-500 hover:bg-red-600 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||||
|
:disabled="isLoadingNetwork"
|
||||||
|
@click="testNetworkError"
|
||||||
|
>
|
||||||
|
{{ isLoadingNetwork ? "테스트 중..." : "네트워크 에러" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||||
|
@click="clearResults"
|
||||||
|
>
|
||||||
|
결과 초기화
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="additionalResults.length > 0" class="mt-4 space-y-3">
|
||||||
|
<h4 class="font-medium text-gray-900">추가 테스트 결과:</h4>
|
||||||
|
<div
|
||||||
|
v-for="(result, index) in additionalResults"
|
||||||
|
:key="index"
|
||||||
|
class="p-3 bg-gray-50 rounded border"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<span class="font-medium text-gray-900 text-sm">{{
|
||||||
|
result.title
|
||||||
|
}}</span>
|
||||||
|
<span class="text-xs text-gray-500">{{
|
||||||
|
result.timestamp
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<pre class="text-xs text-gray-700 whitespace-pre-wrap">{{
|
||||||
|
result.data
|
||||||
|
}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 예제 소스 -->
|
||||||
|
<div class="mt-4 p-3 bg-purple-50 rounded border">
|
||||||
|
<h5 class="font-medium text-purple-900 mb-2">예제 소스:</h5>
|
||||||
|
<pre class="text-xs text-purple-800 whitespace-pre-wrap">
|
||||||
|
{`// 추가 API 테스트 예제
|
||||||
|
const testValidEndpoint = async () => {
|
||||||
|
try {
|
||||||
|
const response = await useApi<ApiResponse<object>>(
|
||||||
|
"/admin/common-codes/USER_STATUS_ACTIVE",
|
||||||
|
{
|
||||||
|
handleError: false,
|
||||||
|
showAlert: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("성공:", response);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("에러:", error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testNetworkError = async () => {
|
||||||
|
try {
|
||||||
|
const response = await useApi<ApiResponse<object>>(
|
||||||
|
"/non-existent-endpoint",
|
||||||
|
{
|
||||||
|
handleError: false,
|
||||||
|
showAlert: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("네트워크 에러:", error.message);
|
||||||
|
}
|
||||||
|
};`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 권한 테스트 섹션 -->
|
||||||
|
<div class="mb-12">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
권한 시스템 테스트
|
||||||
|
</h2>
|
||||||
|
<p class="text-lg text-gray-600">
|
||||||
|
API 권한, 메뉴 권한, 컴포넌트 권한의 동작을 테스트합니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 bg-indigo-500 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-2xl">🔐</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-2xl font-semibold text-gray-900 mb-4">
|
||||||
|
권한 시스템 테스트
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
로그인 후 권한 데이터가 자동으로 로드되며, API 권한, 메뉴 권한,
|
||||||
|
컴포넌트 권한의 동작을 확인할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/permission-test"
|
||||||
|
class="inline-flex items-center bg-indigo-500 hover:bg-indigo-600 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<span class="mr-2">권한 테스트 시작</span>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 사용법 가이드 -->
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-yellow-800 mb-3">
|
||||||
|
사용법 가이드
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold text-yellow-800 mb-2">API 테스트</h4>
|
||||||
|
<div class="text-yellow-700 space-y-2 text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>자동 에러 처리:</strong> useApi 함수가 에러를 자동으로
|
||||||
|
처리하고 사용자에게 알림을 표시합니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>직접 에러 처리:</strong> handleError: false 옵션을
|
||||||
|
사용하여 에러를 직접 처리할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>에러 타입:</strong> 404 (Not Found), 403 (Forbidden),
|
||||||
|
500+ (Server Error) 등 다양한 에러 상황을 테스트할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold text-yellow-800 mb-2">
|
||||||
|
권한 시스템 테스트
|
||||||
|
</h4>
|
||||||
|
<div class="text-yellow-700 space-y-2 text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>API 권한:</strong> 페이지 라우터 접근 권한을 제어합니다.
|
||||||
|
권한이 없으면 홈으로 리다이렉트됩니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>메뉴 권한:</strong> 메뉴 표시 여부를 제어합니다. 권한이
|
||||||
|
없으면 메뉴가 숨겨집니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>컴포넌트 권한:</strong> 버튼 등 UI 컴포넌트의 표시
|
||||||
|
여부를 제어합니다. 권한이 없으면 컴포넌트가 렌더링되지 않습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// import { useUserStore } from "~/stores/user"; // 현재 사용하지 않음
|
||||||
|
|
||||||
|
// 페이지 메타데이터 설정
|
||||||
|
definePageMeta({
|
||||||
|
title: "공용 기능 테스트",
|
||||||
|
description: "API 및 공용 기능들의 동작을 테스트하는 페이지",
|
||||||
|
});
|
||||||
|
|
||||||
|
// const userStore = useUserStore(); // 현재 사용하지 않음
|
||||||
|
|
||||||
|
// 반응형 데이터
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isLoadingCustom = ref(false);
|
||||||
|
const isLoadingValid = ref(false);
|
||||||
|
const isLoadingNetwork = ref(false);
|
||||||
|
const autoErrorResult = ref("");
|
||||||
|
const customErrorResult = ref("");
|
||||||
|
const additionalResults = ref<
|
||||||
|
Array<{ title: string; data: string; timestamp: string }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
// 테스트 다운로드 함수 (자동 에러 처리)
|
||||||
|
const apiTest = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
autoErrorResult.value = "";
|
||||||
|
|
||||||
|
const response = await useApi<ApiResponse<object>>(
|
||||||
|
"/admin/common-codes/USER_STATUS_ACTIVE222"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
autoErrorResult.value = `성공: ${JSON.stringify(response, null, 2)}`;
|
||||||
|
} else {
|
||||||
|
autoErrorResult.value = "응답이 없습니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 직접 에러 처리하는 함수 예시
|
||||||
|
const apiTestWithCustomError = async () => {
|
||||||
|
isLoadingCustom.value = true;
|
||||||
|
customErrorResult.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await useApi<ApiResponse<object>>(
|
||||||
|
"/admin/common-codes/USER_STATUS_ACTIVE222",
|
||||||
|
{
|
||||||
|
handleError: false, // 에러를 직접 처리하겠다는 의미
|
||||||
|
showAlert: false, // alert는 표시하지 않음
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
customErrorResult.value = `성공: ${JSON.stringify(response, null, 2)}`;
|
||||||
|
} else {
|
||||||
|
customErrorResult.value = "응답이 없습니다.";
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// 에러 타입에 처리
|
||||||
|
let errorMessage = "";
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
errorMessage = "[errorCustomHandler]요청한 코드를 찾을 수 없습니다.";
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
errorMessage = "[errorCustomHandler]접근 권한이 없습니다.";
|
||||||
|
} else if (error.response?.status >= 500) {
|
||||||
|
errorMessage =
|
||||||
|
"[errorCustomHandler]서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.";
|
||||||
|
} else {
|
||||||
|
errorMessage = "[errorCustomHandler]알 수 없는 오류가 발생했습니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
customErrorResult.value = `에러 처리됨: ${errorMessage}\n상세 정보: ${JSON.stringify(error.response?.data || error.message, null, 2)}`;
|
||||||
|
} finally {
|
||||||
|
isLoadingCustom.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 유효한 엔드포인트 테스트
|
||||||
|
const testValidEndpoint = async () => {
|
||||||
|
isLoadingValid.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await useApi<ApiResponse<object>>(
|
||||||
|
"/admin/common-codes/USER_STATUS_ACTIVE",
|
||||||
|
{
|
||||||
|
handleError: false,
|
||||||
|
showAlert: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
additionalResults.value.unshift({
|
||||||
|
title: "유효한 엔드포인트 테스트",
|
||||||
|
data: response ? JSON.stringify(response, null, 2) : "응답 없음",
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
additionalResults.value.unshift({
|
||||||
|
title: "유효한 엔드포인트 테스트 (에러)",
|
||||||
|
data: `에러: ${error.message || "알 수 없는 에러"}`,
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isLoadingValid.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 네트워크 에러 테스트
|
||||||
|
const testNetworkError = async () => {
|
||||||
|
isLoadingNetwork.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await useApi<ApiResponse<object>>(
|
||||||
|
"/non-existent-endpoint",
|
||||||
|
{
|
||||||
|
handleError: false,
|
||||||
|
showAlert: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
additionalResults.value.unshift({
|
||||||
|
title: "네트워크 에러 테스트",
|
||||||
|
data: response ? JSON.stringify(response, null, 2) : "응답 없음",
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
additionalResults.value.unshift({
|
||||||
|
title: "네트워크 에러 테스트 (에러)",
|
||||||
|
data: `에러: ${error.message || "알 수 없는 에러"}\n상태 코드: ${error.response?.status || "N/A"}`,
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isLoadingNetwork.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 결과 초기화
|
||||||
|
const clearResults = () => {
|
||||||
|
autoErrorResult.value = "";
|
||||||
|
customErrorResult.value = "";
|
||||||
|
additionalResults.value = [];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 추가 스타일이 필요한 경우 여기에 작성 */
|
||||||
|
</style>
|
||||||
589
pages/admin/permission-test.vue
Normal file
589
pages/admin/permission-test.vue
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4"
|
||||||
|
>
|
||||||
|
<div class="max-w-6xl mx-auto">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
||||||
|
권한 시스템 테스트
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl text-gray-600">
|
||||||
|
페이지 권한, 메뉴 권한, 컴포넌트 권한의 동작을 테스트할 수 있습니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 현재 상태 표시 -->
|
||||||
|
<div class="bg-gray-100 p-4 rounded-lg mb-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">현재 상태</h2>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>
|
||||||
|
<strong>로그인 상태:</strong>
|
||||||
|
<span
|
||||||
|
:class="userStore.isLoggedIn ? 'text-green-600' : 'text-red-600'"
|
||||||
|
>
|
||||||
|
{{ userStore.isLoggedIn ? "로그인됨" : "로그인 안됨" }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="userStore.user">
|
||||||
|
<strong>사용자:</strong> {{ userStore.user.name }} ({{
|
||||||
|
userStore.user.userId
|
||||||
|
}})
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>권한 로딩:</strong>
|
||||||
|
{{ permission.isLoading ? "로딩 중..." : "완료" }}
|
||||||
|
</p>
|
||||||
|
<p v-if="!userStore.isLoggedIn" class="text-orange-600 text-sm">
|
||||||
|
<strong>참고:</strong> 로그인이 필요합니다. 로그인 후 권한 데이터가
|
||||||
|
자동으로 로드됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 권한 테스트 섹션 -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
||||||
|
<!-- 좌측: 권한 상태 및 체크 -->
|
||||||
|
<div class="lg:col-span-2 space-y-6">
|
||||||
|
<!-- 현재 권한 상태 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center mr-3"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-sm">1</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
현재 권한 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
현재 사용자의 권한 상태와 페이지 권한 체크를 확인합니다.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="p-3 bg-gray-50 rounded border">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2">기본 정보:</h4>
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>현재 경로:</strong> <code>{{ $route.path }}</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>로그인 상태:</strong>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
userStore.isLoggedIn ? 'text-green-600' : 'text-red-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ userStore.isLoggedIn ? "로그인됨" : "로그인 안됨" }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="userStore.user">
|
||||||
|
<strong>사용자:</strong> {{ userStore.user.name }} ({{
|
||||||
|
userStore.user.userId
|
||||||
|
}})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-gray-50 rounded border">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2">권한 체크 결과:</h4>
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>페이지 권한({{ $route.path }}):</strong>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
permission.hasPagePermission($route.path)
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
permission.hasPagePermission($route.path)
|
||||||
|
? "있음"
|
||||||
|
: "없음"
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>관리자 메뉴 권한(M01):</strong>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
permission.hasMenuPermission('M01')
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
permission.hasMenuPermission("M01") ? "있음" : "없음"
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 권한별 버튼 테스트 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center mr-3"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-sm">2</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
컴포넌트 권한 테스트
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
각 컴포넌트 권한에 따라 버튼이 표시되거나 숨겨지는 것을
|
||||||
|
확인합니다.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
v-if="permission.hasComponentPermission('C010105')"
|
||||||
|
class="bg-green-500 text-white px-3 py-1 rounded text-sm hover:bg-green-600"
|
||||||
|
>
|
||||||
|
생성 버튼
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>생성 버튼 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
v-if="permission.hasComponentPermission('C010102')"
|
||||||
|
class="bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
수정 버튼
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>수정 버튼 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
v-if="permission.hasComponentPermission('C010101')"
|
||||||
|
class="bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600"
|
||||||
|
>
|
||||||
|
삭제 버튼
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>삭제 버튼 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
v-if="permission.hasComponentPermission('C010103')"
|
||||||
|
class="bg-purple-500 text-white px-3 py-1 rounded text-sm hover:bg-purple-600"
|
||||||
|
>
|
||||||
|
내보내기 버튼
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>내보내기 버튼 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
v-if="permission.hasComponentPermission('C010104')"
|
||||||
|
class="bg-orange-500 text-white px-3 py-1 rounded text-sm hover:bg-orange-600"
|
||||||
|
>
|
||||||
|
가져오기 버튼
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>가져오기 버튼 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
v-if="permission.hasComponentPermission('C010106')"
|
||||||
|
class="bg-gray-500 text-white px-3 py-1 rounded text-sm hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
보기 버튼
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>보기 버튼 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 메뉴 권한 테스트 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-orange-500 rounded-full flex items-center justify-center mr-3"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-sm">3</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
메뉴 권한 테스트
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
메뉴 권한에 따라 메뉴가 표시되거나 숨겨지는 것을 확인합니다.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
v-if="permission.hasMenuPermission('M01')"
|
||||||
|
class="bg-blue-100 text-blue-800 px-3 py-1 rounded text-sm"
|
||||||
|
>
|
||||||
|
관리자 메뉴
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>관리자 메뉴 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
v-if="permission.hasMenuPermission('M02')"
|
||||||
|
class="bg-green-100 text-green-800 px-3 py-1 rounded text-sm"
|
||||||
|
>
|
||||||
|
사용자 메뉴
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>사용자 메뉴 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
v-if="permission.hasMenuPermission('M03')"
|
||||||
|
class="bg-purple-100 text-purple-800 px-3 py-1 rounded text-sm"
|
||||||
|
>
|
||||||
|
설정 메뉴
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>설정 메뉴 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
v-if="permission.hasMenuPermission('M04')"
|
||||||
|
class="bg-yellow-100 text-yellow-800 px-3 py-1 rounded text-sm"
|
||||||
|
>
|
||||||
|
보고서 메뉴
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400 text-sm"
|
||||||
|
>보고서 메뉴 (권한 없음)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 우측: 권한 체크 로직 테스트 -->
|
||||||
|
<div class="lg:col-span-1">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-purple-500 rounded-full flex items-center justify-center mr-3"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-sm">4</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
권한 체크 로직
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
다양한 권한 체크 함수들의 동작을 테스트합니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="p-3 bg-gray-50 rounded border">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2 text-sm">
|
||||||
|
페이지 권한 체크:
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-gray-600 mb-1">
|
||||||
|
<code>hasPagePermission('/admin/codes')</code>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-sm font-semibold"
|
||||||
|
:class="
|
||||||
|
permission.hasPagePermission('/admin/codes')
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
permission.hasPagePermission("/admin/codes")
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-gray-50 rounded border">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2 text-sm">
|
||||||
|
메뉴 권한 체크:
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-gray-600 mb-1">
|
||||||
|
<code>hasMenuPermission('M000001')</code>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-sm font-semibold"
|
||||||
|
:class="
|
||||||
|
permission.hasMenuPermission('M000001')
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
permission.hasMenuPermission("M000001") ? "true" : "false"
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-gray-50 rounded border">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2 text-sm">
|
||||||
|
여러 권한 중 하나라도 있는지:
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-gray-600 mb-1">
|
||||||
|
<code>hasAnyComponentPermission(['C010105', 'C010102'])</code>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-sm font-semibold"
|
||||||
|
:class="
|
||||||
|
permission.hasAnyComponentPermission(['C010105', 'C010102'])
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
permission.hasAnyComponentPermission(["C010105", "C010102"])
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-gray-50 rounded border">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2 text-sm">
|
||||||
|
모든 권한이 있는지:
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-gray-600 mb-1">
|
||||||
|
<code
|
||||||
|
>hasAllComponentPermissions(['C010105', 'C010102'])</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-sm font-semibold"
|
||||||
|
:class="
|
||||||
|
permission.hasAllComponentPermissions([
|
||||||
|
'C010105',
|
||||||
|
'C010102',
|
||||||
|
])
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
permission.hasAllComponentPermissions([
|
||||||
|
"C010105",
|
||||||
|
"C010102",
|
||||||
|
])
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API 권한 테스트 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-indigo-500 rounded-full flex items-center justify-center mr-3"
|
||||||
|
>
|
||||||
|
<span class="text-white font-bold text-sm">5</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
페이지 권한 테스트
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
다양한 페이지 경로에 대한 권한을 테스트합니다. 권한이 없으면 홈으로
|
||||||
|
리다이렉트됩니다.
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="pagePath in [
|
||||||
|
'/',
|
||||||
|
'/login',
|
||||||
|
'/register',
|
||||||
|
'/about',
|
||||||
|
'/sampleList',
|
||||||
|
'/admin/codes',
|
||||||
|
'/admin/logs',
|
||||||
|
'/admin/programs',
|
||||||
|
'/admin/resource',
|
||||||
|
'/admin/permission-test',
|
||||||
|
'/test/culture-graph',
|
||||||
|
'/test/pathway',
|
||||||
|
'/test/test01',
|
||||||
|
'/test/common-test',
|
||||||
|
'/popup/addSamplePopup',
|
||||||
|
'/nonexistent',
|
||||||
|
]"
|
||||||
|
:key="pagePath"
|
||||||
|
class="p-3 rounded border text-center"
|
||||||
|
:class="
|
||||||
|
permission.hasPagePermission(pagePath)
|
||||||
|
? 'bg-green-50 border-green-200'
|
||||||
|
: 'bg-red-50 border-red-200'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="text-sm font-medium text-gray-900 mb-1">
|
||||||
|
{{ pagePath }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-xs"
|
||||||
|
:class="
|
||||||
|
permission.hasPagePermission(pagePath)
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
permission.hasPagePermission(pagePath)
|
||||||
|
? "✓ 접근 가능"
|
||||||
|
: "✗ 접근 불가"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 p-3 bg-blue-50 rounded border">
|
||||||
|
<p class="text-sm text-blue-800">
|
||||||
|
<strong>참고:</strong> 실제로는 권한이 없는 경로에 접근하면 자동으로
|
||||||
|
홈으로 리다이렉트됩니다. 이 페이지는
|
||||||
|
<code>/admin/permission-test</code> 경로로, 페이지 권한이 있어야
|
||||||
|
접근할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 권한 데이터 표시 -->
|
||||||
|
<div
|
||||||
|
v-if="userStore.isLoggedIn"
|
||||||
|
class="bg-white rounded-lg shadow-md p-6 mb-8"
|
||||||
|
>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">
|
||||||
|
현재 권한 데이터
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<!-- 페이지 권한 -->
|
||||||
|
<div class="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<h4 class="text-lg font-semibold mb-3 text-blue-800">
|
||||||
|
페이지 권한
|
||||||
|
</h4>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="page in permission.permissions.resources.pages"
|
||||||
|
:key="page.oid"
|
||||||
|
class="text-sm bg-white p-2 rounded border"
|
||||||
|
>
|
||||||
|
<div class="font-medium">{{ page.name }} ({{ page.code }})</div>
|
||||||
|
<div class="text-gray-600">{{ page.path }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 메뉴 권한 -->
|
||||||
|
<div class="bg-green-50 p-4 rounded-lg">
|
||||||
|
<h4 class="text-lg font-semibold mb-3 text-green-800">메뉴 권한</h4>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="menu in permission.permissions.resources.menus"
|
||||||
|
:key="menu.oid"
|
||||||
|
class="text-sm bg-white p-2 rounded border"
|
||||||
|
>
|
||||||
|
<div class="font-medium">{{ menu.name }} ({{ menu.code }})</div>
|
||||||
|
<div class="text-gray-600">{{ menu.description }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 컴포넌트 권한 -->
|
||||||
|
<div class="bg-purple-50 p-4 rounded-lg">
|
||||||
|
<h4 class="text-lg font-semibold mb-3 text-purple-800">
|
||||||
|
컴포넌트 권한
|
||||||
|
</h4>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="component in permission.permissions.resources.components"
|
||||||
|
:key="component.oid"
|
||||||
|
class="text-sm bg-white p-2 rounded border"
|
||||||
|
>
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ component.name }} ({{ component.code }})
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-600">
|
||||||
|
{{ component.componentType }} - {{ component.description }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 사용법 가이드 -->
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-yellow-800 mb-3">
|
||||||
|
권한 시스템 가이드
|
||||||
|
</h3>
|
||||||
|
<div class="text-yellow-700 space-y-2">
|
||||||
|
<p>
|
||||||
|
<strong>로그인 필요:</strong> 이 페이지를 사용하려면 먼저 로그인해야
|
||||||
|
합니다. 로그인 시 가데이터 권한이 자동으로 로드됩니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>페이지 권한:</strong> 페이지 라우터 접근 권한을 제어합니다.
|
||||||
|
권한이 없으면 홈으로 리다이렉트됩니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>메뉴 권한:</strong> 메뉴 표시 여부를 제어합니다. 권한이
|
||||||
|
없으면 메뉴가 숨겨집니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>컴포넌트 권한:</strong> 버튼 등 UI 컴포넌트의 표시 여부를
|
||||||
|
제어합니다. 권한이 없으면 컴포넌트가 렌더링되지 않습니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>백엔드 연동:</strong> 나중에 백엔드 API가 준비되면
|
||||||
|
<code>stores/permissions.ts</code>의
|
||||||
|
<code>fetchPermissions</code> 함수를 수정하여 실제 API를 호출하도록
|
||||||
|
변경할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 페이지 메타데이터 설정
|
||||||
|
definePageMeta({
|
||||||
|
title: "권한 시스템 테스트",
|
||||||
|
description:
|
||||||
|
"페이지 권한, 메뉴 권한, 컴포넌트 권한의 동작을 테스트하는 페이지",
|
||||||
|
});
|
||||||
|
|
||||||
|
const permission = usePermission();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 이 페이지는 /admin 경로이므로 페이지 권한이 필요합니다
|
||||||
|
// middleware/auth.ts에서 자동으로 권한을 체크합니다
|
||||||
|
// 로그인 시 권한 데이터가 자동으로 로드됩니다
|
||||||
|
</script>
|
||||||
230
stores/permissions.ts
Normal file
230
stores/permissions.ts
Normal file
@@ -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<UserPermissions>({
|
||||||
|
resources: {
|
||||||
|
menus: [],
|
||||||
|
pages: [],
|
||||||
|
components: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 권한 로딩 상태
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
// 서버에서 권한 데이터 가져오기 (현재는 가데이터 사용)
|
||||||
|
const fetchPermissions = async (): Promise<boolean> => {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -9,13 +9,14 @@ export const useUserStore = defineStore(
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const token = ref<string | null>(null);
|
const token = ref<string | null>(null);
|
||||||
|
|
||||||
// 추후 제거 필요
|
// 권한 스토어 참조
|
||||||
const isAdmin = true;
|
const permissionsStore = usePermissionsStore();
|
||||||
|
|
||||||
interface LoginData {
|
interface LoginData {
|
||||||
userId: string;
|
userId: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async (userId: string, password: string) => {
|
const login = async (userId: string, password: string) => {
|
||||||
const { success, data, description } = await useApi<
|
const { success, data, description } = await useApi<
|
||||||
ApiResponse<LoginData>
|
ApiResponse<LoginData>
|
||||||
@@ -27,6 +28,10 @@ export const useUserStore = defineStore(
|
|||||||
if (success) {
|
if (success) {
|
||||||
user.value = data;
|
user.value = data;
|
||||||
isLoggedIn.value = true;
|
isLoggedIn.value = true;
|
||||||
|
|
||||||
|
// 로그인 성공 시 권한 데이터 가져오기
|
||||||
|
await permissionsStore.fetchPermissions();
|
||||||
|
|
||||||
return { success, data };
|
return { success, data };
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
@@ -39,21 +44,13 @@ export const useUserStore = defineStore(
|
|||||||
const logout = () => {
|
const logout = () => {
|
||||||
user.value = null;
|
user.value = null;
|
||||||
isLoggedIn.value = false;
|
isLoggedIn.value = false;
|
||||||
|
|
||||||
|
// 권한 데이터도 초기화
|
||||||
|
permissionsStore.clearPermissions();
|
||||||
|
|
||||||
useApi("/members/logout", { method: "post" });
|
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) => {
|
const setToken = (accessToken: string) => {
|
||||||
token.value = accessToken;
|
token.value = accessToken;
|
||||||
};
|
};
|
||||||
@@ -62,24 +59,15 @@ export const useUserStore = defineStore(
|
|||||||
return token;
|
return token;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 초기 인증 상태 확인
|
|
||||||
if (import.meta.client) {
|
|
||||||
checkAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 상태
|
// 상태
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
user,
|
user,
|
||||||
token,
|
token,
|
||||||
|
|
||||||
// 게터
|
|
||||||
isAdmin,
|
|
||||||
|
|
||||||
// 액션
|
// 액션
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
checkAuth,
|
|
||||||
setToken,
|
setToken,
|
||||||
getToken,
|
getToken,
|
||||||
};
|
};
|
||||||
|
|||||||
345
types/permissions.ts
Normal file
345
types/permissions.ts
Normal file
@@ -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: "테스트 상세보기 버튼",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user