tab, component, popup 변경
This commit is contained in:
114
README.md
114
README.md
@@ -5,8 +5,25 @@
|
||||
- compnenets 아래의 .vue는 자동 인식(template 내부에서만, 별도 script에서 필요시 선언 필요)
|
||||
|
||||
|
||||
## 공통 페이지 구성
|
||||
# 구성 요소
|
||||
## components 구성
|
||||
```
|
||||
components
|
||||
|- base // 기본 요소(button, input, grid, popup)
|
||||
|- layout // 레이아웃 요소(header, footer, sidebar, wrapper)
|
||||
|- module // 특정 기능 단위(card, form, list)
|
||||
|- pages // 특정 페이지 전용
|
||||
```
|
||||
|
||||
## page 구성
|
||||
```
|
||||
pages // 단일 화면(비 탭 요소)
|
||||
|- [tabId] // 탭 요소
|
||||
|- admin // 관리자 페이지
|
||||
```
|
||||
|
||||
# page(페이지) 생성 요소
|
||||
## 공통 페이지 구성
|
||||
```
|
||||
<template>
|
||||
<ContentsWrapper> <!-- wrapper(title) 추가 -->
|
||||
@@ -27,6 +44,7 @@
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
## Toast(Tui) Grid 사용법
|
||||
한글 설명: https://github.com/nhn/tui.grid/blob/master/packages/toast-ui.grid/docs/v4.0-migration-guide-kor.md
|
||||
|
||||
@@ -185,3 +203,97 @@ const treeColumnOptions = { name: 'name', useCascadingCheckbox: true };
|
||||
const grid1Ref = ref();
|
||||
</script>
|
||||
```
|
||||
|
||||
## 팝업 구성
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ customPopup.vue │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ poupWrapper.vue │ │
|
||||
│ │ ┌─────────────────────┐ │ │
|
||||
│ │ │ <slot> │ │ │
|
||||
│ │ └─────────────────────┘ │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────────────────┘
|
||||
|
||||
|
||||
customPopup.vue - 기본 빈 팝업
|
||||
poupWrapper.vue - top, middle, bottom으로 구성된 팝업 구조
|
||||
|
||||
1. 구조에 따라 customPopup, poupWrapper 기반으로 팝업 생성(/pages/popup/*.vue)
|
||||
2. 사용할 페이지에서 생성한 팝업 추가
|
||||
```
|
||||
|
||||
|
||||
### 팝업 생성
|
||||
```
|
||||
// examplePopup.vue
|
||||
<template>
|
||||
<div>
|
||||
<!--PopupWrapper 구성, 크기 지정, 숨김 여부 지정 -->
|
||||
<PopupWrapper width="1000px" height="600px" v-model:show="show">
|
||||
<template #top>
|
||||
<h2>팝업 제목</h2>
|
||||
</template>
|
||||
|
||||
<template #middle>
|
||||
<!-- 팝업 본문 -->
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
<button>추가 버튼</button>
|
||||
<!-- 닫기 버튼은 자동 생성 -->
|
||||
</template>
|
||||
</PopupWrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 숨김 여부 지정
|
||||
const show = defineModel('show', {type: Boolean, default:false});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 팝업 사용
|
||||
```
|
||||
<template>
|
||||
<button @click="popupShow = true">팝업 실행</button>
|
||||
<examplePopup v-model:show="popupShow" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import addSamplePopup from '../popup/examplePopup.vue';
|
||||
const popupShow = ref(false);
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
## 탭 구성
|
||||
```
|
||||
// layouts/default.vue
|
||||
// stores/tab.ts
|
||||
|
||||
import { useTabsStore } from "../stores/tab";
|
||||
const tabsStore = useTabsStore();
|
||||
|
||||
// 탭추가 (최대 10개)
|
||||
tabsStore.addTab();
|
||||
|
||||
// 탭 갱신
|
||||
tabsStore.updateActiveTab({ label, to, componentName});
|
||||
|
||||
// 탭 변경
|
||||
tabsStore.setActiveTab(key);
|
||||
|
||||
// 탭 생성
|
||||
<div
|
||||
v-for="tab in tabsStore.tabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ active: tabsStore.activeTab === 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>
|
||||
</div>
|
||||
```
|
@@ -11,6 +11,8 @@ interface TreeColumnOptions {
|
||||
|
||||
const tuiGridRef = ref<TuiGridElement>();
|
||||
|
||||
const selectionUnit = "row"
|
||||
|
||||
const props = defineProps<{
|
||||
data: OptRow[];
|
||||
// editor: https://github.com/nhn/tui.grid/blob/master/packages/toast-ui.grid/docs/v4.0-migration-guide-kor.md
|
||||
@@ -43,5 +45,6 @@ function clearGrid() {
|
||||
:treeColumnOptions="props.treeColumnOptions"
|
||||
:rowHeaders="props.rowHeaders"
|
||||
:rowKey="props.rowKey"
|
||||
:selectionUnit="selectionUnit"
|
||||
/>
|
||||
</template>
|
28
components/base/customPopup.vue
Normal file
28
components/base/customPopup.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div v-if="show" class="popup-overlay" @click.self="show = false">
|
||||
<div class="popup-container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const show = defineModel('show', {type: Boolean, default:false});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.popup-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.popup-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
81
components/layout/PopupWrapper.vue
Normal file
81
components/layout/PopupWrapper.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<customPopup v-model:show="show">
|
||||
<div class="popup-content" :style="{ width, height }">
|
||||
<!-- Top -->
|
||||
<div class="popup-top">
|
||||
<slot name="top"></slot>
|
||||
</div>
|
||||
|
||||
<!-- Middle -->
|
||||
<div class="popup-middle">
|
||||
<slot name="middle"></slot>
|
||||
</div>
|
||||
|
||||
<!-- Bottom -->
|
||||
<div class="popup-bottom">
|
||||
<button class="popup-close" @click="show = false">닫기</button>
|
||||
<slot name="bottom"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</customPopup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
defineProps<{
|
||||
width?: string
|
||||
height?: string
|
||||
}>()
|
||||
|
||||
// defineModel + 기본값 지정
|
||||
const show = defineModel('show', {type: Boolean, default:false});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.popup-content {
|
||||
background: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.popup-top {
|
||||
padding: 10px 20px;
|
||||
font-weight: bold;
|
||||
background: #f0f0f0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.popup-middle {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.popup-bottom {
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: center; /* 중앙 정렬 */
|
||||
gap: 10px;
|
||||
background: #f9f9f9;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* ⭐️ bottom 슬롯 버튼 공통 스타일 */
|
||||
.popup-bottom ::v-deep(button) {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.popup-bottom ::v-deep(button:hover) {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
background: #ddd !important;
|
||||
color: black !important;
|
||||
}
|
||||
</style>
|
15
composables/popupManager.ts
Normal file
15
composables/popupManager.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
let baseZIndex = 1000;
|
||||
|
||||
++baseZIndex;
|
||||
|
||||
const currentZ = ref(baseZIndex)
|
||||
|
||||
export function usePopupZIndex() {
|
||||
function nextZIndex() {
|
||||
currentZ.value += 1
|
||||
return currentZ.value
|
||||
}
|
||||
return { nextZIndex }
|
||||
}
|
@@ -1,52 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import AppHeader from "../components/AppHeader.vue";
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import AppHeader from "../components/layout/AppHeader.vue";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useTabsStore } from "../stores/tab";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const activeMenu = ref("home");
|
||||
const showSubmenuBar = ref(false);
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
|
||||
// HOME 메뉴가 선택되었을 때 최상단 경로로 이동
|
||||
// 메뉴 클릭 시 홈 이동
|
||||
watch(activeMenu, (newValue) => {
|
||||
if (newValue === "home") {
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
|
||||
watch(route, () => {
|
||||
showSubmenuBar.value = false;
|
||||
if (newValue === "home") router.push("/");
|
||||
});
|
||||
|
||||
// 서브메뉴 정의
|
||||
const subMenus = computed(() => {
|
||||
if (activeMenu.value === "test") {
|
||||
return [
|
||||
{ key: "test", label: "테스트", to: "/test/test01" },
|
||||
{ key: "igv", label: "ivg", to: "/test/test02" },
|
||||
{ key: "igv2", label: "ivg2", 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" },
|
||||
];
|
||||
} 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 [{
|
||||
key: "test",
|
||||
label: "테스트",
|
||||
to: "/test/test01"
|
||||
}, {
|
||||
key: "igv",
|
||||
label: "ivg",
|
||||
to: "/test/test02"
|
||||
}, {
|
||||
key: "igv2",
|
||||
label: "ivg2",
|
||||
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"
|
||||
}, ];
|
||||
} 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 [];
|
||||
});
|
||||
|
||||
@@ -55,29 +98,19 @@ function onMenuClick(menu: string) {
|
||||
showSubmenuBar.value = true;
|
||||
}
|
||||
|
||||
// 서브메뉴 클릭 시 탭 추가
|
||||
function onSubMenuClick(sub: { key: string; label: string; to: string, componentName:string }) {
|
||||
tabsStore.addTab(sub);
|
||||
router.push(sub.to);
|
||||
// ✅ 서브메뉴 클릭 → 현재 활성 탭 내용만 변경
|
||||
function onSubMenuClick(sub: { key: string; label: string; to: string; componentName: string }) {
|
||||
tabsStore.updateActiveTab(sub);
|
||||
// const activeKey = tabsStore.activeTab;
|
||||
// router.push(`/${activeKey}${sub.to}`);
|
||||
}
|
||||
|
||||
function handleClickOutsideSubmenuBar(event: MouseEvent) {
|
||||
const submenu = document.querySelector(".submenu-bar");
|
||||
if (
|
||||
submenu &&
|
||||
!submenu.contains(event.target as Node) &&
|
||||
!(event.target as HTMLElement).classList.contains("menu-btn")
|
||||
) {
|
||||
showSubmenuBar.value = false;
|
||||
}
|
||||
// ✅ 새 탭 추가 버튼
|
||||
function addNewTab() {
|
||||
tabsStore.addTab();
|
||||
// router.push(`/${key}/`);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("click", handleClickOutsideSubmenuBar);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("click", handleClickOutsideSubmenuBar);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -85,46 +118,41 @@ onBeforeUnmount(() => {
|
||||
<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"
|
||||
class="submenu-btn"
|
||||
:class="{ active: $route.path === sub.to }"
|
||||
@click="onSubMenuClick({...sub, componentName : sub.key})"
|
||||
@click="onSubMenuClick({ ...sub, componentName: sub.key })"
|
||||
>
|
||||
{{ sub.label }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- 동적 탭 바 -->
|
||||
<div v-if="tabsStore.tabs.length" class="tab-bar">
|
||||
<br><br>
|
||||
<!-- 탭 바 -->
|
||||
<div class="tab-bar">
|
||||
<div
|
||||
v-for="tab in tabsStore.tabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ active: tabsStore.activeTab === tab.key }"
|
||||
@click="tabsStore.setActiveTab(tab.key); router.push(tab.to)"
|
||||
@click="tabsStore.setActiveTab(tab.key);"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<span 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>
|
||||
|
||||
<!-- ✅ 새 탭 추가 버튼 -->
|
||||
<button class="add-tab-btn" @click="addNewTab">+</button>
|
||||
</div>
|
||||
|
||||
<main class="main">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2024 Nuxt.js App</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
min-height: 100vh;
|
||||
|
@@ -44,5 +44,8 @@ export default defineNuxtConfig({
|
||||
shim: false,
|
||||
strict: true,
|
||||
},
|
||||
plugins: ['~/plugins/vue3-tui-grid.client.ts']
|
||||
plugins: ['~/plugins/vue3-tui-grid.client.ts'],
|
||||
components: [
|
||||
{ path: '~/components', pathPrefix: false }, // 경로 접두사 제거
|
||||
]
|
||||
});
|
||||
|
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {colDefs} from '../../composables/grids/resourceGrid'
|
||||
import {colDefs} from '../../../composables/grids/resourceGrid'
|
||||
|
||||
definePageMeta({
|
||||
title: '리소스 관리'
|
123
pages/[tabId]/index.vue
Normal file
123
pages/[tabId]/index.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4"
|
||||
>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
||||
Integrated Bio Foundry Platform
|
||||
</h1>
|
||||
<p class="text-xl text-gray-600">
|
||||
통합 바이오 파운드리 플랫폼에 오신 것을 환영합니다
|
||||
</p>
|
||||
|
||||
<!-- 사용자 환영 메시지 -->
|
||||
<div
|
||||
v-if="userStore.isLoggedIn"
|
||||
class="mt-6 p-4 bg-white rounded-lg shadow-md inline-block"
|
||||
>
|
||||
<p class="text-lg text-gray-800 mb-2">
|
||||
안녕하세요,
|
||||
<span class="font-semibold text-blue-600">{{
|
||||
userStore.userName
|
||||
}}</span
|
||||
>님!
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ userStore.isAdmin ? "관리자" : "사용자" }} 권한으로
|
||||
로그인되었습니다.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="mt-6 p-4 bg-yellow-50 rounded-lg shadow-md inline-block"
|
||||
>
|
||||
<p class="text-lg text-gray-800 mb-2">로그인이 필요합니다</p>
|
||||
<NuxtLink
|
||||
to="/login"
|
||||
class="inline-block mt-2 bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
로그인하기
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tailwind CSS 테스트 섹션 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center mb-4 mx-auto"
|
||||
>
|
||||
<span class="text-white font-bold">1</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Feature 1</h3>
|
||||
<p class="text-gray-600">Tailwind CSS가 정상 작동하고 있습니다!</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 bg-green-500 rounded-full flex items-center justify-center mb-4 mx-auto"
|
||||
>
|
||||
<span class="text-white font-bold">2</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Feature 2</h3>
|
||||
<p class="text-gray-600">반응형 디자인이 적용되었습니다.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 bg-purple-500 rounded-full flex items-center justify-center mb-4 mx-auto"
|
||||
>
|
||||
<span class="text-white font-bold">3</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Feature 3</h3>
|
||||
<p class="text-gray-600">모던한 UI 컴포넌트를 사용할 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 테스트 -->
|
||||
<div class="text-center space-x-4">
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Primary Button
|
||||
</button>
|
||||
<button
|
||||
class="bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Secondary Button
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "~/stores/user";
|
||||
|
||||
// 페이지 메타데이터 설정
|
||||
definePageMeta({
|
||||
title: "Home",
|
||||
description: "Welcome to our Nuxt.js application",
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #00dc82;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
20
pages/[tabId]/sampleList.vue
Normal file
20
pages/[tabId]/sampleList.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<ContentsWrapper> <!-- wrapper(title) 추가 -->
|
||||
<template #actions> <!--title 우측 버튼 설정-->
|
||||
<button>스케쥴 확인</button>
|
||||
<button @click="addSamplePopupShow = true">샘플 등록</button>
|
||||
</template>
|
||||
<!--메인 콘텐츠 영역-->
|
||||
<input type="text" >
|
||||
<addSamplePopup v-model:show="addSamplePopupShow" />
|
||||
</ContentsWrapper>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import addSamplePopup from '../popup/addSamplePopup.vue';
|
||||
// title(wrapper) 설정
|
||||
definePageMeta({
|
||||
title: '조회 결과'
|
||||
})
|
||||
|
||||
const addSamplePopupShow = ref(false);
|
||||
</script>
|
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ToastGrid from '../components/ToastGrid.vue';
|
||||
import ToastGrid from '@/components/base/ToastGrid.vue';
|
||||
|
||||
const data = [
|
||||
{
|
@@ -100,6 +100,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "~/stores/user";
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(()=> {
|
||||
router.push('/1/')
|
||||
})
|
||||
|
||||
|
||||
// 페이지 메타데이터 설정
|
||||
definePageMeta({
|
||||
@@ -108,6 +115,7 @@ definePageMeta({
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
38
pages/popup/addSamplePopup.vue
Normal file
38
pages/popup/addSamplePopup.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<PopupWrapper width="1000px" height="600px" v-model:show="show">
|
||||
<template #top>
|
||||
<h2>의뢰 선택</h2>
|
||||
</template>
|
||||
|
||||
<template #middle>
|
||||
<ToastGrid :ref="sampleListRef" :data="data" :columns="columns" :rowHeaders="rowHeader"/>
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
<button>선택</button>
|
||||
</template>
|
||||
</PopupWrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const show = defineModel('show', {type: Boolean, default:false});
|
||||
|
||||
|
||||
const sampleListRef = ref();
|
||||
|
||||
const data= [
|
||||
{name : "TEXT", user : "TEXT", end : "TEXT" },
|
||||
{name : "TEXT", user : "TEXT", end : "TEXT" }
|
||||
];
|
||||
|
||||
const rowHeader = ['rowNum'];
|
||||
|
||||
const columns = [
|
||||
{ header: '의뢰 이름', name: 'name', width: 300 },
|
||||
{ header: '의뢰자', name: 'user' },
|
||||
{ header: '마감일자', name: 'end' },
|
||||
]
|
||||
</script>
|
7
stores/router.client.ts
Normal file
7
stores/router.client.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
provide: {
|
||||
router: useRouter()
|
||||
}
|
||||
}
|
||||
})
|
@@ -1,36 +1,64 @@
|
||||
// stores/tabs.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export interface TabItem {
|
||||
key: string
|
||||
label: string
|
||||
to: string
|
||||
componentName: string
|
||||
interface Tab {
|
||||
key: number; // 1~10
|
||||
label: string;
|
||||
to: string; // 페이지 라우트
|
||||
componentName: string;
|
||||
}
|
||||
|
||||
export const useTabsStore = defineStore('tabs', {
|
||||
const defaultTab = { key: 1, label: "홈", to: "/", componentName: "home" };
|
||||
|
||||
export const useTabsStore = defineStore("tabs", {
|
||||
state: () => ({
|
||||
activeTab: '' as string,
|
||||
tabs: [] as { key: string; label: string; to: string; componentName: string }[]
|
||||
tabs: [defaultTab] as Tab[],
|
||||
activeTab: 1
|
||||
}),
|
||||
actions: {
|
||||
addTab(tab: TabItem) {
|
||||
if (!this.tabs.find(t => t.key === tab.key)) {
|
||||
this.tabs.push(tab)
|
||||
// ✅ 새 탭 추가 (기본 페이지는 "/")
|
||||
addTab() {
|
||||
const { $router } = useNuxtApp();
|
||||
|
||||
if (this.tabs.length >= 10) {
|
||||
alert("탭은 최대 10개까지 열 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
this.activeTab = tab.key
|
||||
// 빈 key 찾기
|
||||
let key = 1;
|
||||
while (this.tabs.find(t => t.key === key)) key++;
|
||||
|
||||
this.tabs.push({...defaultTab, key : key});
|
||||
this.activeTab = key;
|
||||
$router.push(defaultTab.to);
|
||||
return key;
|
||||
},
|
||||
removeTab(key: string) {
|
||||
const idx = this.tabs.findIndex(t => t.key === key)
|
||||
if (idx !== -1) {
|
||||
this.tabs.splice(idx, 1)
|
||||
if (this.activeTab === key && this.tabs.length) {
|
||||
this.activeTab = this.tabs[Math.max(idx - 1, 0)].key
|
||||
}
|
||||
|
||||
// ✅ 활성 탭 내용 변경 (서브메뉴 클릭)
|
||||
updateActiveTab(sub: { label: string; to: string; componentName: string }) {
|
||||
const { $router } = useNuxtApp();
|
||||
|
||||
const tab = this.tabs.find(t => t.key === this.activeTab);
|
||||
if (tab) {
|
||||
tab.label = sub.label;
|
||||
tab.to = sub.to;
|
||||
tab.componentName = sub.componentName;
|
||||
}
|
||||
$router.push(`/${tab?.key}${tab?.to}`);
|
||||
},
|
||||
|
||||
removeTab(key: number) {
|
||||
this.tabs = this.tabs.filter(t => t.key !== key);
|
||||
if (this.activeTab === key) {
|
||||
this.activeTab = this.tabs.length ? this.tabs[this.tabs.length - 1].key : 0;
|
||||
}
|
||||
},
|
||||
setActiveTab(key: string) {
|
||||
this.activeTab = key
|
||||
|
||||
setActiveTab(key: number) {
|
||||
const { $router } = useNuxtApp();
|
||||
this.activeTab = key;
|
||||
|
||||
const tab = this.tabs.find(t => t.key === this.activeTab);
|
||||
$router.push(`/${tab?.key}${tab?.to}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@@ -10,64 +10,50 @@ export const useUserStore = defineStore("user", () => {
|
||||
} | null>(null);
|
||||
const token = ref<string | null>(null);
|
||||
|
||||
|
||||
interface LoginData {
|
||||
userId: string
|
||||
role: string
|
||||
lastLoginAt: string
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
code: number
|
||||
message: string
|
||||
description: string
|
||||
data: LoginData
|
||||
}
|
||||
|
||||
// 게터
|
||||
const isAdmin = computed(() => user.value?.role === "admin");
|
||||
const userName = computed(() => user.value?.name || "사용자");
|
||||
|
||||
// 액션
|
||||
const login = async (userId: string, password: string) => {
|
||||
try {
|
||||
// 실제 API 호출로 대체할 수 있습니다
|
||||
/*
|
||||
const { data, error: _error } = await useApi('/login', {
|
||||
|
||||
const {data, error: _error } = await useApi<LoginResponse>('/service/login', {
|
||||
method: 'post',
|
||||
body: { id: userId, pw: password, loginLogFlag : 0 }
|
||||
body: { userId, password }
|
||||
})
|
||||
*/
|
||||
// 임시 로그인 로직 (실제로는 API 응답을 사용)
|
||||
if (userId && password) {
|
||||
// 테스트 계정 확인
|
||||
let mockUser;
|
||||
|
||||
if (userId === "admin" && password === "stam1201!") {
|
||||
mockUser = {
|
||||
id: "1",
|
||||
userId: "admin",
|
||||
email: "admin@test.com",
|
||||
name: "관리자",
|
||||
role: "admin",
|
||||
};
|
||||
} else if (userId === "user" && password === "stam1201!") {
|
||||
mockUser = {
|
||||
id: "2",
|
||||
userId: "user",
|
||||
email: "user@test.com",
|
||||
name: "일반사용자",
|
||||
role: "user",
|
||||
};
|
||||
} else {
|
||||
throw new Error("아이디 또는 비밀번호가 올바르지 않습니다.");
|
||||
}
|
||||
let mockUser;
|
||||
|
||||
/*
|
||||
if(data && data.value){
|
||||
mockUser = data.value;
|
||||
}else{
|
||||
throw new Error("아이디 또는 비밀번호가 올바르지 않습니다.");
|
||||
}
|
||||
*/
|
||||
user.value = mockUser;
|
||||
token.value = "mock-token-" + Date.now();
|
||||
isLoggedIn.value = true;
|
||||
|
||||
// 로컬 스토리지에 저장
|
||||
localStorage.setItem("user", JSON.stringify(mockUser));
|
||||
localStorage.setItem("token", token.value);
|
||||
|
||||
return { success: true, user: mockUser };
|
||||
} else {
|
||||
throw new Error("아이디와 비밀번호를 입력해주세요.");
|
||||
if(data && data.value && data.value.code === 200){
|
||||
mockUser = data.value.data;
|
||||
}else{
|
||||
throw new Error("아이디 또는 비밀번호가 올바르지 않습니다.");
|
||||
}
|
||||
|
||||
user.value = mockUser;
|
||||
token.value = "mock-token-" + Date.now();
|
||||
isLoggedIn.value = true;
|
||||
|
||||
// 로컬 스토리지에 저장
|
||||
localStorage.setItem("user", JSON.stringify(mockUser));
|
||||
localStorage.setItem("token", token.value);
|
||||
|
||||
return { success: true, user: mockUser };
|
||||
} catch (error) {
|
||||
console.error("로그인 실패:", error);
|
||||
return {
|
||||
|
Reference in New Issue
Block a user