[UI 개선] 기본 레이아웃에서 서브메뉴 및 탭 기능 개선, 공용 기능 테스트 페이지 추가

This commit is contained in:
2025-09-22 11:20:12 +09:00
parent 30e2099d14
commit 221e250814
2 changed files with 490 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import AppHeader from "../components/layout/AppHeader.vue";
import { ref, computed, watch } from "vue";
import { ref, computed, watch } from "vue";
import { useRouter } from "vue-router";
import { useTabsStore } from "../stores/tab";
@@ -11,85 +11,109 @@ const showSubmenuBar = ref(false);
const tabsStore = useTabsStore();
// 메뉴 클릭 시 홈 이동
watch(activeMenu, (newValue) => {
watch(activeMenu, newValue => {
if (newValue === "home") router.push("/");
});
// 서브메뉴 정의
const subMenus = computed(() => {
if (activeMenu.value === "test") {
return [{
return [
{
key: "test",
label: "테스트",
to: "/test/test01"
}, {
to: "/test/test01",
},
{
key: "igv",
label: "ivg",
to: "/test/test02"
}, {
to: "/test/test02",
},
{
key: "igv2",
label: "ivg2",
to: "/test/igv2"
}, {
to: "/test/igv2",
},
{
key: "pathway",
label: "pathway",
to: "/test/pathway"
}, {
to: "/test/pathway",
},
{
key: "pathway2",
label: "pathway2",
to: "/test/pathway2"
}, {
to: "/test/pathway2",
},
{
key: "pathway3",
label: "pathway3",
to: "/test/pathway3"
}, {
to: "/test/pathway3",
},
{
key: "pathway4",
label: "pathway4",
to: "/cultureGraph/pathway4"
}, {
to: "/cultureGraph/pathway4",
},
{
key: "pathwayjson",
label: "pathwayjson",
to: "/test/pathwayjson"
}, {
to: "/test/pathwayjson",
},
{
key: "cultureGraph",
label: "배양그래프",
to: "/test/culture-graph"
}, {
to: "/test/culture-graph",
},
{
key: "cultureGraphMulti",
label: "배양그래프 멀티",
to: "/test/culture-graph-multi"
}, {
to: "/test/culture-graph-multi",
},
{
key: "cultureGraphTab",
label: "배양그래프 탭",
to: "/test/culture-graph-tab"
}, {
to: "/test/culture-graph-tab",
},
{
key: "tui-grid",
label: "tui-grid",
to: "/tui"
}, {
to: "/tui",
},
{
key: "리소스",
label: "리소스",
to: "/admin/resource"
}, {
to: "/admin/resource",
},
{
key: "sample",
label: "sample",
to: "/sampleList"
}, ];
} else if (activeMenu.value === "ADMIN") {
return [{
to: "/sampleList",
},
{
key: "common-test",
label: "공용 기능 테스트",
to: "/common-test",
},
];
} else if (activeMenu.value === "ADMIN") {
return [
{
key: "logs",
label: "접속기록",
to: "/admin/logs"
}, {
to: "/admin/logs",
},
{
key: "codes",
label: "공통코드",
to: "/admin/codes"
}, {
to: "/admin/codes",
},
{
key: "programs",
label: "프로그램",
to: "/admin/programs"
}, ];
}
to: "/admin/programs",
},
];
}
return [];
});
@@ -99,7 +123,12 @@ function onMenuClick(menu: string) {
}
// ✅ 서브메뉴 클릭 → 현재 활성 탭 내용만 변경
function onSubMenuClick(sub: { key: string; label: string; to: string; componentName: string }) {
function onSubMenuClick(sub: {
key: string;
label: string;
to: string;
componentName: string;
}) {
tabsStore.updateActiveTab(sub);
// const activeKey = tabsStore.activeTab;
// router.push(`/${activeKey}${sub.to}`);
@@ -110,7 +139,6 @@ function addNewTab() {
tabsStore.addTab();
// router.push(`/${key}/`);
}
</script>
<template>
@@ -118,7 +146,11 @@ function addNewTab() {
<AppHeader v-model="activeMenu" @update:model-value="onMenuClick" />
<!-- 서브메뉴 -->
<nav v-if="subMenus && subMenus.length && showSubmenuBar" class="submenu-bar" @click.stop>
<nav
v-if="subMenus && subMenus.length && showSubmenuBar"
class="submenu-bar"
@click.stop
>
<button
v-for="sub in subMenus"
:key="sub.key"
@@ -128,7 +160,7 @@ function addNewTab() {
{{ sub.label }}
</button>
</nav>
<br><br>
<br /><br />
<!-- -->
<div class="tab-bar">
<div
@@ -136,10 +168,16 @@ function addNewTab() {
:key="tab.key"
class="tab-item"
:class="{ active: tabsStore.activeTab === tab.key }"
@click="tabsStore.setActiveTab(tab.key);"
@click="tabsStore.setActiveTab(tab.key)"
>
{{ tab.label }}
<span v-show="tabsStore.activeTab !== tab.key" class="close-btn" @click.stop="tabsStore.removeTab(tab.key)"> × </span>
<span
v-show="tabsStore.activeTab !== tab.key"
class="close-btn"
@click.stop="tabsStore.removeTab(tab.key)"
>
×
</span>
</div>
<!-- 추가 버튼 -->
@@ -152,7 +190,6 @@ function addNewTab() {
</div>
</template>
<style scoped>
.layout {
min-height: 100vh;

View File

@@ -0,0 +1,406 @@
<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);
}
};`}</apiresponse<object></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("알 수 없는 오류가 발생했습니다.");
}
}
};`}</apiresponse<object></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);
}
};`}</apiresponse<object></apiresponse<object></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 = "";
try {
const response = await useApi<ApiResponse<object>>(
"/admin/common-codes/USER_STATUS_ACTIVE222"
);
if (response) {
autoErrorResult.value = `성공: ${JSON.stringify(response, null, 2)}`;
} else {
autoErrorResult.value = "응답이 없습니다.";
}
} catch (error: any) {
autoErrorResult.value = `에러 발생: ${error.message || "알 수 없는 에러"}`;
} finally {
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>